From 7ec41fba4380dec71b75cdbc2bb1db48f05e88ab Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Tue, 14 Aug 2018 12:13:22 +0100 Subject: [PATCH 01/16] Config file and base dir can be specified together (#3783) --- node/src/main/kotlin/net/corda/node/NodeArgsParser.kt | 7 +++---- node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt | 7 ------- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt index c2d93f7879..eb6e35ad12 100644 --- a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt @@ -61,11 +61,10 @@ class NodeArgsParser : AbstractArgsParser() { override fun doParse(optionSet: OptionSet): CmdLineOptions { require(optionSet.nonOptionArguments().isEmpty()) { "Unrecognized argument(s): ${optionSet.nonOptionArguments().joinToString(separator = ", ")}"} - require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) { - "${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together" - } + val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath() - val configFile = baseDirectory / optionSet.valueOf(configFileArg) + val configFilePath = Paths.get(optionSet.valueOf(configFileArg)) + val configFile = if (configFilePath.isAbsolute) configFilePath else baseDirectory / configFilePath.toString() val loggingLevel = optionSet.valueOf(loggerLevel) val logToConsole = optionSet.has(logToConsoleArg) val isRegistration = optionSet.has(isRegistrationArg) diff --git a/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt b/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt index c47c1e7aeb..bdd34d7bba 100644 --- a/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt +++ b/node/src/test/kotlin/net/corda/node/NodeArgsParserTest.kt @@ -80,13 +80,6 @@ class NodeArgsParserTest { assertThat(cmdLineOptions.configFile).isEqualTo(configFile) } - @Test - fun `both base-directory and config-file`() { - assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { - parser.parse("--base-directory", "base", "--config-file", "conf") - }.withMessageContaining("base-directory").withMessageContaining("config-file") - } - @Test fun `base-directory without argument`() { assertThatExceptionOfType(OptionException::class.java).isThrownBy { From f979d9d3cf24670acd06e10c564cd0aad3534d50 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Tue, 14 Aug 2018 18:33:12 +0100 Subject: [PATCH 02/16] Include the deterministic test-data.jar in the build's assembly. (#3784) --- core-deterministic/testing/data/build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/core-deterministic/testing/data/build.gradle b/core-deterministic/testing/data/build.gradle index 7b94ae6c74..d203ae5572 100644 --- a/core-deterministic/testing/data/build.gradle +++ b/core-deterministic/testing/data/build.gradle @@ -23,6 +23,7 @@ test { includeTestsMatching "net.corda.deterministic.data.GenerateData" } } +assemble.finalizedBy test artifacts { testData file: file("$buildDir/test-data.jar"), type: 'jar', builtBy: test From 7a1b75ef35deece96a0e5b49f42a1bfabcf766b4 Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 15 Aug 2018 10:22:09 +0100 Subject: [PATCH 03/16] [CORDA-1822]: Derive error code from exception signature (#3774) --- .../kotlin/net/corda/core/utilities/Try.kt | 31 ++- .../net/corda/node/internal/NodeStartup.kt | 186 +++++++++--------- 2 files changed, 126 insertions(+), 91 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/utilities/Try.kt b/core/src/main/kotlin/net/corda/core/utilities/Try.kt index 7d2d678e5f..d6b5dbdf14 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Try.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Try.kt @@ -3,8 +3,6 @@ package net.corda.core.utilities import net.corda.core.KeepForDJVM import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.Try.Failure -import net.corda.core.utilities.Try.Success /** * Representation of an operation that has either succeeded with a result (represented by [Success]) or failed with an @@ -60,6 +58,35 @@ sealed class Try { is Failure -> uncheckedCast(this) } + /** Applies the given action to the value if [Success], or does nothing if [Failure]. Returns `this` for chaining. */ + fun doOnSuccess(action: (A) -> Unit): Try { + when (this) { + is Success -> action.invoke(value) + is Failure -> {} + } + return this + } + + /** Applies the given action to the error if [Failure], or does nothing if [Success]. Returns `this` for chaining. */ + fun doOnFailure(action: (Throwable) -> Unit): Try { + when (this) { + is Success -> {} + is Failure -> action.invoke(exception) + } + return this + } + + /** Applies the given action to the exception if [Failure], rethrowing [Error]s. Does nothing if [Success]. Returns `this` for chaining. */ + fun doOnException(action: (Exception) -> Unit): Try { + return doOnFailure { error -> + if (error is Exception) { + action.invoke(error) + } else { + throw error + } + } + } + @KeepForDJVM data class Success(val value: A) : Try() { override val isSuccess: Boolean get() = true diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt index 3e5441a516..f5b2ad7a87 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -97,102 +97,108 @@ open class NodeStartup(val args: Array) { drawBanner(versionInfo) Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) - val conf = try { - val (rawConfig, conf0Result) = loadConfigFile(cmdlineOptions) - if (cmdlineOptions.devMode) { - println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") - } - val conf0 = conf0Result.getOrThrow() - if (cmdlineOptions.bootstrapRaftCluster) { - if (conf0 is NodeConfigurationImpl) { - println("Bootstrapping raft cluster (starting up as seed node).") - // Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining. - conf0.copy(notary = conf0.notary?.copy(raft = conf0.notary?.raft?.copy(clusterAddresses = emptyList()))) - } else { - println("bootstrap-raft-notaries flag not recognized, exiting...") - return false - } - } else { - conf0 - } - } catch (e: UnknownConfigurationKeysException) { - logger.error(e.message) - return false - } catch (e: ConfigException.IO) { - println(""" - Unable to load the node config file from '${cmdlineOptions.configFile}'. - Try experimenting with the --base-directory flag to change which directory the node - is looking in, or use the --config-file flag to specify it explicitly. - """.trimIndent()) - return false - } catch (e: Exception) { - logger.error("Unexpected error whilst reading node configuration", e) - return false - } - val errors = conf.validate() + val configuration = (attempt { loadConfiguration(cmdlineOptions) }.doOnException(handleConfigurationLoadingError(cmdlineOptions.configFile)) as? Try.Success)?.let(Try.Success::value) ?: return false + + val errors = configuration.validate() if (errors.isNotEmpty()) { - logger.error("Invalid node configuration. Errors where:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") + logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") return false } - try { - banJavaSerialisation(conf) - preNetworkRegistration(conf) - if (cmdlineOptions.nodeRegistrationOption != null) { - // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] - registerWithNetwork(conf, versionInfo, cmdlineOptions.nodeRegistrationOption) - // At this point the node registration was successful. We can delete the marker file. - deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory) - return true - } - logStartupInfo(versionInfo, cmdlineOptions, conf) - } catch (e: NodeRegistrationException) { - logger.warn("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.", e) - return false - } catch (e: Exception) { - logger.error("Exception during node registration", e) - return false + attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return false + + attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return false + + cmdlineOptions.nodeRegistrationOption?.let { + // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] + attempt { registerWithNetwork(configuration, versionInfo, cmdlineOptions.nodeRegistrationOption) }.doOnException(handleRegistrationError) as? Try.Success ?: return false + + // At this point the node registration was successful. We can delete the marker file. + deleteNodeRegistrationMarker(cmdlineOptions.baseDirectory) + return true } - try { - cmdlineOptions.baseDirectory.createDirectories() - startNode(conf, versionInfo, startTime, cmdlineOptions) - } catch (e: MultipleCordappsForFlowException) { - logger.error(e.message) - return false - } catch (e: CouldNotCreateDataSourceException) { - logger.error(e.message, e.cause) - return false - } catch (e: CheckpointIncompatibleException) { - logger.error(e.message) - return false - } catch (e: AddressBindingException) { - logger.error(e.message) - return false - } catch (e: NetworkParametersReader.Error) { - logger.error(e.message) - return false - } catch (e: DatabaseIncompatibleException) { - e.message?.let { Node.printWarning(it) } - logger.error(e.message) - return false - } catch (e: Exception) { - if (e is Errors.NativeIoException && e.message?.contains("Address already in use") == true) { - logger.error("One of the ports required by the Corda node is already in use.") - return false - } - if (e.message?.startsWith("Unknown named curve:") == true) { - logger.error("Exception during node startup - ${e.message}. " + - "This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") - } else { - logger.error("Exception during node startup", e) - } - return false - } + logStartupInfo(versionInfo, cmdlineOptions, configuration) - logger.info("Node exiting successfully") - return true + return attempt { startNode(configuration, versionInfo, startTime, cmdlineOptions) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError).isSuccess + } + + private fun attempt(action: () -> RESULT): Try = Try.on(action) + + private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) } + + private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class) + + private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]") + + private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message [errorCode=${errorCode()}]", error) + + private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true + + private fun Exception.errorCode(): String { + + val hash = staticLocationBasedHash() + return Integer.toOctalString(hash) + } + + private fun Throwable.staticLocationBasedHash(visited: Set = setOf(this)): Int { + + val cause = this.cause + return when { + cause != null && !visited.contains(cause) -> Objects.hash(this::class.java.name, stackTrace, cause.staticLocationBasedHash(visited + cause)) + else -> Objects.hash(this::class.java.name, stackTrace) + } + } + + private val handleRegistrationError = { error: Exception -> + when (error) { + is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.") + else -> error.logAsUnexpected("Exception during node registration") + } + } + + private val handleStartError = { error: Exception -> + when { + error.isExpectedWhenStartingNode() -> error.logAsExpected() + error is CouldNotCreateDataSourceException -> error.logAsUnexpected() + error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.") + error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") + else -> error.logAsUnexpected("Exception during node startup") + } + } + + private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception -> + when (error) { + is UnknownConfigurationKeysException -> error.logAsExpected() + is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println) + else -> error.logAsUnexpected("Unexpected error whilst reading node configuration") + } + } + + private fun configFileNotFoundMessage(configFile: Path): String { + return """ + Unable to load the node config file from '$configFile'. + + Try setting the --base-directory flag to change which directory the node + is looking in, or use the --config-file flag to specify it explicitly. + """.trimIndent() + } + + private fun loadConfiguration(cmdlineOptions: CmdLineOptions): NodeConfiguration { + + val (rawConfig, configurationResult) = loadConfigFile(cmdlineOptions) + if (cmdlineOptions.devMode) { + println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") + } + val configuration = configurationResult.getOrThrow() + return if (cmdlineOptions.bootstrapRaftCluster) { + println("Bootstrapping raft cluster (starting up as seed node).") + // Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining. + (configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList()))) + } else { + configuration + } } private fun checkRegistrationMode(): Boolean { @@ -234,7 +240,7 @@ open class NodeStartup(val args: Array) { marker.delete() } } catch (e: Exception) { - logger.warn("Could not delete the marker file that was created for `--initial-registration`.", e) + e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn) } } @@ -243,6 +249,8 @@ open class NodeStartup(val args: Array) { protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { + + cmdlineOptions.baseDirectory.createDirectories() val node = createNode(conf, versionInfo) if (cmdlineOptions.clearNetworkMapCache) { node.clearNetworkMapCache() From 2b69789a30f14cda1bc3744f943a55ed2e68f634 Mon Sep 17 00:00:00 2001 From: josecoll Date: Wed, 15 Aug 2018 15:15:11 +0100 Subject: [PATCH 04/16] Fix Vault contractStateType's bootstrapping bug, and add integration test. (#3785) --- .../corda/services/vault/VaultRestartTest.kt | 48 +++++++++++++++++++ .../node/services/vault/NodeVaultService.kt | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 node/src/integration-test/kotlin/net/corda/services/vault/VaultRestartTest.kt diff --git a/node/src/integration-test/kotlin/net/corda/services/vault/VaultRestartTest.kt b/node/src/integration-test/kotlin/net/corda/services/vault/VaultRestartTest.kt new file mode 100644 index 0000000000..a7d5650f2e --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/services/vault/VaultRestartTest.kt @@ -0,0 +1,48 @@ +package net.corda.services.vault + +import net.corda.core.CordaRuntimeException +import net.corda.core.contracts.FungibleAsset +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.vaultQueryBy +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.getOrThrow +import net.corda.finance.DOLLARS +import net.corda.finance.contracts.asset.Cash +import net.corda.finance.flows.CashIssueFlow +import net.corda.testing.core.DUMMY_BANK_A_NAME +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.OutOfProcess +import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions +import org.junit.Test + +class VaultRestartTest { + + @Test + fun `restart and query vault after adding some cash states`() { + driver(DriverParameters(inMemoryDB = false, startNodesInProcess = false, + extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"))) { + val node = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to "localhost:30000")).getOrThrow() + + val expected = 500.DOLLARS + val ref = OpaqueBytes.of(0x01) + val notary = node.rpc.notaryIdentities().firstOrNull() ?: throw CordaRuntimeException("Missing notary") + val issueTx = node.rpc.startFlow(::CashIssueFlow, expected, ref, notary).returnValue.getOrThrow() + println("Issued transaction: $issueTx") + + // Query vault + Assertions.assertThat(node.rpc.vaultQueryBy().states).hasSize(1) + Assertions.assertThat(node.rpc.vaultQueryBy>().states).hasSize(1) + + // Restart the node and re-query the vault + println("Shutting down the node ...") + (node as OutOfProcess).process.destroyForcibly() + node.stop() + + println("Restarting the node ...") + val restartedNode = startNode(providedName = DUMMY_BANK_A_NAME, customOverrides = mapOf("p2pAddress" to "localhost:30000")).getOrThrow() + Assertions.assertThat(restartedNode.rpc.vaultQueryBy().states).hasSize(1) + Assertions.assertThat(restartedNode.rpc.vaultQueryBy>().states).hasSize(1) + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 8cc9634425..1412bcba8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -535,7 +535,7 @@ class NodeVaultService( val contractTypes = deriveContractTypes(it) contractTypes.map { val contractStateType = contractStateTypeMappings.getOrPut(it.name) { mutableSetOf() } - contractStateType.add(it.name) + contractStateType.add(concreteType.name) } } } From e5e4596461a2026d481acfef4a3c21bbced5d03c Mon Sep 17 00:00:00 2001 From: Michele Sollecito Date: Wed, 15 Aug 2018 17:02:25 +0100 Subject: [PATCH 05/16] [CORDA-1681]: It is not possible to run stateMachinesSnapshot from the shell (fixed). (#3791) --- .../main/kotlin/net/corda/core/context/InvocationContext.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt index 64cdee0792..b9f66ca423 100644 --- a/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt +++ b/core/src/main/kotlin/net/corda/core/context/InvocationContext.kt @@ -103,7 +103,8 @@ sealed class InvocationOrigin { /** * Origin was an RPC call. */ - data class RPC(private val actor: Actor) : InvocationOrigin() { + // Field `actor` needs to stay public for AMQP / JSON serialization to work. + data class RPC(val actor: Actor) : InvocationOrigin() { override fun principal() = Principal { actor.id.value } } From 9c9e8dab4051d6a30026ca05acfa03cea25fb75b Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 16 Aug 2018 10:01:26 +0100 Subject: [PATCH 06/16] Fixes formatting. (#3793) --- docs/source/cordapp-build-systems.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 29b2a351ab..1548cf0085 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -187,5 +187,5 @@ These files are loaded when a CorDapp context is created and so can change durin 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 -`_. +There is an example project that demonstrates in ``samples`` called ``cordapp-configuration`` and API documentation in +``_. From fffa0638031c6601995fa51901a612c581fc1aa6 Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Thu, 16 Aug 2018 15:44:40 +0100 Subject: [PATCH 07/16] Cli backwards compatibility testing (#3733) * first pass at infrastructure around cli compatibility * add example unit test * inspect enum types * add a basic unit test to verify behaviour of the cli checker * revert root build.gradle --- .idea/compiler.xml | 2 + settings.gradle | 3 +- testing/test-cli/build.gradle | 17 ++ .../testing/CliBackwardsCompatibleTest.kt | 19 ++ .../testing/GenerateCommandLineCompat.kt | 188 ++++++++++++++++++ .../CommandLineCompatibilityCheckerTest.kt | 106 ++++++++++ .../resources/net.corda.testing.Dummy.yml | 77 +++++++ 7 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 testing/test-cli/build.gradle create mode 100644 testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt create mode 100644 testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt create mode 100644 testing/test-cli/src/test/kotlin/net/corda/testing/CommandLineCompatibilityCheckerTest.kt create mode 100644 testing/test-cli/src/test/resources/net.corda.testing.Dummy.yml diff --git a/.idea/compiler.xml b/.idea/compiler.xml index bc63206a42..1c4c966a8b 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -185,6 +185,8 @@ + + diff --git a/settings.gradle b/settings.gradle index fd98fba0ab..8433c9477e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -24,11 +24,12 @@ include 'experimental:kryo-hook' include 'experimental:corda-utils' include 'jdk8u-deterministic' include 'test-common' +include 'test-cli' include 'test-utils' include 'smoke-test-utils' include 'node-driver' // Avoid making 'testing' a project, and allow build.gradle files to refer to these by their simple names: -['test-common', 'test-utils', 'smoke-test-utils', 'node-driver'].each { +['test-common', 'test-utils', 'test-cli', 'smoke-test-utils', 'node-driver'].each { project(":$it").projectDir = new File("$settingsDir/testing/$it") } include 'tools:explorer' diff --git a/testing/test-cli/build.gradle b/testing/test-cli/build.gradle new file mode 100644 index 0000000000..462499bf64 --- /dev/null +++ b/testing/test-cli/build.gradle @@ -0,0 +1,17 @@ +apply plugin: 'java' +apply plugin: 'kotlin' + +dependencies { + compile group: 'info.picocli', name: 'picocli', version: '3.0.1' + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile group: "com.fasterxml.jackson.dataformat", name: "jackson-dataformat-yaml", version: "2.9.0" + compile group: "com.fasterxml.jackson.core", name: "jackson-databind", version: "2.9.0" + compile "com.fasterxml.jackson.module:jackson-module-kotlin:2.9.+" + compile "junit:junit:$junit_version" + +} +compileKotlin { + kotlinOptions { + languageVersion = "1.2" + } +} \ No newline at end of file diff --git a/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt b/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt new file mode 100644 index 0000000000..53b4b1b032 --- /dev/null +++ b/testing/test-cli/src/main/kotlin/net/corda/testing/CliBackwardsCompatibleTest.kt @@ -0,0 +1,19 @@ +package net.corda.testing + +import junit.framework.AssertionFailedError + +open class CliBackwardsCompatibleTest { + + + fun checkBackwardsCompatibility(clazz: Class<*>) { + val checker = CommandLineCompatibilityChecker() + val checkResults = checker.checkCommandLineIsBackwardsCompatible(clazz) + + if (checkResults.isNotEmpty()) { + val exceptionMessage= checkResults.map { it.message }.joinToString(separator = "\n") + throw AssertionFailedError("Command line is not backwards compatible:\n$exceptionMessage") + } + } + + +} \ No newline at end of file diff --git a/testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt b/testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt new file mode 100644 index 0000000000..e0141767c5 --- /dev/null +++ b/testing/test-cli/src/main/kotlin/net/corda/testing/GenerateCommandLineCompat.kt @@ -0,0 +1,188 @@ +package net.corda.testing + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.module.kotlin.registerKotlinModule +import picocli.CommandLine +import java.io.InputStream +import java.util.* +import kotlin.collections.ArrayList + + +class CommandLineCompatibilityChecker { + + fun topoSort(commandLine: CommandLine): List { + val toVisit = Stack() + toVisit.push(commandLine) + val sorted: MutableList = ArrayList(); + while (toVisit.isNotEmpty()) { + val visiting = toVisit.pop() + sorted.add(visiting) + visiting.subcommands.values.sortedBy { it.commandName }.forEach { + toVisit.push(it) + } + } + return buildDescriptors(sorted) + } + + private fun buildDescriptors(result: MutableList): List { + return result.map { ::parseToDescription.invoke(it) } + } + + internal fun parseToDescription(it: CommandLine): CommandDescription { + val commandSpec = it.commandSpec + val options = commandSpec.options().filterNot { it.usageHelp() || it.versionHelp() } + .map { hit -> hit.names().map { it to hit } } + .flatMap { it } + .sortedBy { it.first } + .map { + val type = it.second.type() + ParameterDescription(it.first, type.componentType?.canonicalName + ?: type.canonicalName, it.second.required(), isMultiple(type), determineAcceptableOptions(type)) + } + + val positionals = commandSpec.positionalParameters().sortedBy { it.index() }.map { + val type = it.type() + ParameterDescription(it.index().toString(), type.componentType?.canonicalName + ?: type.canonicalName, it.required(), isMultiple(type)) + } + return CommandDescription(it.commandName, positionals, options) + } + + private fun determineAcceptableOptions(type: Class<*>?): List { + return if (type?.isEnum == true) { + type.enumConstants.map { it.toString() } + } else { + emptyList() + } + } + + fun isMultiple(clazz: Class<*>): Boolean { + return Iterable::class.java.isAssignableFrom(clazz) || Array::class.java.isAssignableFrom(clazz) + } + + fun printCommandDescription(commandLine: CommandLine) { + val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() + val results = topoSort(commandLine) + println(objectMapper.writeValueAsString(results)) + } + + fun readCommandDescription(inputStream: InputStream): List { + val objectMapper = ObjectMapper(YAMLFactory()).registerKotlinModule() + return objectMapper.readValue>(inputStream, object : TypeReference>() {}); + } + + fun checkAllCommandsArePresent(old: List, new: List): List { + val oldSet = old.map { it.commandName }.toSet() + val newSet = new.map { it.commandName }.toSet() + val newIsSuperSetOfOld = newSet.containsAll(oldSet) + return if (!newIsSuperSetOfOld) { + oldSet.filterNot { newSet.contains(it) }.map { + CommandsChangedError("SubCommand: $it has been removed from the CLI") + } + } else { + emptyList() + } + } + + fun checkAllOptionsArePresent(old: CommandDescription, new: CommandDescription): List { + if (old.commandName != new.commandName) { + throw IllegalArgumentException("Commands must match (${old.commandName} != ${new.commandName})") + } + val oldSet = old.params.map { it.parameterName }.toSet() + val newSet = new.params.map { it.parameterName }.toSet() + + val newIsSuperSetOfOld = newSet.containsAll(oldSet) + + return if (!newIsSuperSetOfOld) { + oldSet.filterNot { newSet.contains(it) }.map { + OptionsChangedError("Parameter: $it has been removed from subcommand: ${old.commandName}") + } + } else { + emptyList() + } + } + + fun checkAllPositionalCharactersArePresent(old: CommandDescription, new: CommandDescription): List { + if (old.commandName != new.commandName) { + throw IllegalArgumentException("Commands must match (${old.commandName} != ${new.commandName})") + } + val oldSet = old.positionalParams.sortedBy { it.parameterName }.toSet() + val newSet = new.positionalParams.sortedBy { it.parameterName}.toSet() + val newIsSuperSetOfOld = newSet.containsAll(oldSet) + return if (!newIsSuperSetOfOld) { + oldSet.filterNot { newSet.contains(it) }.map { + PositionalArgumentsChangedError("Positional Parameter [ ${it.parameterName} ] has been removed from subcommand: ${old.commandName}") + } + } else { + emptyList() + } + } + + fun checkAllParamsAreOfTheSameType(old: CommandDescription, new: CommandDescription): List { + + val oldMap = old.params.map { it.parameterName to it.parameterType }.toMap() + val newMap = new.params.map { it.parameterName to it.parameterType }.toMap() + + val changedTypes = oldMap.filter { newMap[it.key] != null && newMap[it.key] != it.value }.map { + TypesChangedError("Parameter [ ${it.key} has changed from type: ${it.value} to ${newMap[it.key]}") + } + val oldAcceptableTypes = old.params.map { it.parameterName to it.acceptableValues }.toMap() + val newAcceptableTypes = new.params.map { it.parameterName to it.acceptableValues }.toMap() + val potentiallyChanged = oldAcceptableTypes.filter { newAcceptableTypes[it.key] != null && newAcceptableTypes[it.key]!!.toSet() != it.value.toSet() } + val missingEnumErrors = potentiallyChanged.map { + val oldEnums = it.value + val newEnums = newAcceptableTypes[it.key]!! + if (!newEnums.containsAll(oldEnums)) { + val toPrint = oldEnums.toMutableSet() + toPrint.removeAll(newAcceptableTypes[it.key]!!) + EnumOptionsChangedError(it.key + " on command ${old.commandName} previously accepted: $oldEnums, and now is missing $toPrint}") + } else { + null + } + }.filterNotNull() + return changedTypes + missingEnumErrors + + } + + fun checkCommandLineIsBackwardsCompatible(commandLineToCheck: Class<*>): List { + val commandLineToCheckName = commandLineToCheck.canonicalName + val instance = commandLineToCheck.newInstance() + val resourceAsStream = this.javaClass.classLoader.getResourceAsStream("$commandLineToCheckName.yml") + ?: throw IllegalStateException("no Descriptor for $commandLineToCheckName found on classpath") + val old = readCommandDescription(resourceAsStream) + val new = topoSort(CommandLine(instance)) + return checkCommandLineIsBackwardsCompatible(old, new) + } + + + fun checkBackwardsCompatibility(old: CommandLine, new: CommandLine): List { + val topoSortOld= topoSort(old) + val topoSortNew= topoSort(new) + return checkCommandLineIsBackwardsCompatible(topoSortOld, topoSortNew) + } + + private fun checkCommandLineIsBackwardsCompatible(old: List, new: List): List { + val results = ArrayList() + results += checkAllCommandsArePresent(old, new) + for (oldCommand in old) { + new.find { it.commandName == oldCommand.commandName }?.let { newCommand -> + results += checkAllOptionsArePresent(oldCommand, newCommand) + results += checkAllParamsAreOfTheSameType(oldCommand, newCommand) + results += checkAllPositionalCharactersArePresent(oldCommand, newCommand) + } + } + + return results + } +} + +open class CliBackwardsCompatibilityValidationCheck(val message: String) +class OptionsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error) +class TypesChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error) +class EnumOptionsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error) +class CommandsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error) +class PositionalArgumentsChangedError(error: String) : CliBackwardsCompatibilityValidationCheck(error) +data class CommandDescription(val commandName: String, val positionalParams: List, val params: List) +data class ParameterDescription(val parameterName: String, val parameterType: String, val required: Boolean, val multiParam: Boolean, val acceptableValues: List = emptyList()) \ No newline at end of file diff --git a/testing/test-cli/src/test/kotlin/net/corda/testing/CommandLineCompatibilityCheckerTest.kt b/testing/test-cli/src/test/kotlin/net/corda/testing/CommandLineCompatibilityCheckerTest.kt new file mode 100644 index 0000000000..bf3a3cf653 --- /dev/null +++ b/testing/test-cli/src/test/kotlin/net/corda/testing/CommandLineCompatibilityCheckerTest.kt @@ -0,0 +1,106 @@ +package net.corda.testing + +import org.hamcrest.CoreMatchers.* +import org.junit.Assert +import org.junit.Test +import picocli.CommandLine +import java.util.regex.Pattern + +class CommandLineCompatibilityCheckerTest { + + enum class AllOptions { + YES, NO, MAYBZ + } + + enum class BinaryOptions { + YES, NO + } + + + @Test + fun `should detect missing parameter`() { + val value1 = object { + @CommandLine.Option(names = arrayOf("-d", "--directory"), description = arrayOf("the directory to run in")) + var baseDirectory: String? = null + } + val value2 = object { + @CommandLine.Option(names = arrayOf("--directory"), description = arrayOf("the directory to run in")) + var baseDirectory: String? = null + } + val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2)) + Assert.assertThat(breaks.size, `is`(1)) + Assert.assertThat(breaks.first(), `is`(instanceOf(OptionsChangedError::class.java))) + } + + + @Test + fun `should detect changes in positional parameters`() { + val value1 = object { + @CommandLine.Parameters(index = "0") + var baseDirectory: String? = null + @CommandLine.Parameters(index = "1") + var depth: Pattern? = null + } + val value2 = object { + @CommandLine.Parameters(index = "1") + var baseDirectory: String? = null + @CommandLine.Parameters(index = "0") + var depth: Int? = null + } + val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2)) + Assert.assertThat(breaks.size, `is`(2)) + Assert.assertThat(breaks.first(), `is`(instanceOf(PositionalArgumentsChangedError::class.java))) + } + + @Test + fun `should detect removal of a subcommand`() { + @CommandLine.Command(subcommands = [ListCommand::class, StatusCommand::class]) + class Dummy + + @CommandLine.Command(subcommands = [ListCommand::class]) + class Dummy2 + + val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(Dummy()), CommandLine(Dummy2())) + Assert.assertThat(breaks.size, `is`(1)) + Assert.assertThat(breaks.first(), `is`(instanceOf(CommandsChangedError::class.java))) + } + + @Test + fun `should detect change of parameter type`() { + val value1 = object { + @CommandLine.Option(names = ["--directory"], description = ["the directory to run in"]) + var baseDirectory: String? = null + } + val value2 = object { + @CommandLine.Option(names = ["--directory"], description = ["the directory to run in"]) + var baseDirectory: Pattern? = null + } + + val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2)) + Assert.assertThat(breaks.size, `is`(1)) + Assert.assertThat(breaks.first(), `is`(instanceOf(TypesChangedError::class.java))) + } + + @Test + fun `should detect change of enum options`() { + val value1 = object { + @CommandLine.Option(names = ["--directory"], description = ["the directory to run in"]) + var baseDirectory: AllOptions? = null + } + val value2 = object { + @CommandLine.Option(names = ["--directory"], description = ["the directory to run in"]) + var baseDirectory: BinaryOptions? = null + } + + val breaks = CommandLineCompatibilityChecker().checkBackwardsCompatibility(CommandLine(value1), CommandLine(value2)) + Assert.assertThat(breaks.filter { it is EnumOptionsChangedError }.size, `is`(1)) + Assert.assertThat(breaks.first { it is EnumOptionsChangedError }.message, containsString(AllOptions.MAYBZ.name)) + } + + @CommandLine.Command(name = "status") + class StatusCommand + + @CommandLine.Command(name = "ls") + class ListCommand + +} \ No newline at end of file diff --git a/testing/test-cli/src/test/resources/net.corda.testing.Dummy.yml b/testing/test-cli/src/test/resources/net.corda.testing.Dummy.yml new file mode 100644 index 0000000000..fa0f03a014 --- /dev/null +++ b/testing/test-cli/src/test/resources/net.corda.testing.Dummy.yml @@ -0,0 +1,77 @@ +- commandName: "
" + positionalParams: + - parameterName: "0" + parameterType: "java.net.InetAddress" + required: true + multiParam: false + acceptableValues: [] + - parameterName: "1" + parameterType: "int" + required: true + multiParam: false + acceptableValues: [] + params: + - parameterName: "--directory" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-d" + parameterType: "java.lang.String" + required: false + multiParam: false + acceptableValues: [] +- commandName: "status" + positionalParams: [] + params: + - parameterName: "--pattern" + parameterType: "java.lang.String" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "--style" + parameterType: "net.corda.testing.DummyEnum" + required: false + multiParam: false + acceptableValues: + - "FULL" + - "DIR" + - "FILE" + - "DISK" + - parameterName: "-p" + parameterType: "java.lang.String" + required: false + multiParam: true + acceptableValues: [] + - parameterName: "-s" + parameterType: "net.corda.testing.DummyEnum" + required: false + multiParam: false + acceptableValues: + - "FULL" + - "DIR" + - "FILE" + - "DISK" +- commandName: "ls" + positionalParams: + - parameterName: "0" + parameterType: "java.lang.String" + required: true + multiParam: false + acceptableValues: [] + - parameterName: "1" + parameterType: "int" + required: true + multiParam: false + acceptableValues: [] + params: + - parameterName: "--depth" + parameterType: "java.lang.Integer" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-d" + parameterType: "java.lang.Integer" + required: false + multiParam: false + acceptableValues: [] \ No newline at end of file From f3392e31d251a6bd8b57d4d31eaadb8b1a229c87 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Thu, 16 Aug 2018 17:34:50 +0100 Subject: [PATCH 08/16] Documents waitQuiescent. Explains why it is important. (#3787) * Update api-testing.rst * Warning regarding using threadPerNode * Corrects JavaDocs. Adds message to . --- docs/source/api-testing.rst | 21 +++++++++++-------- .../net/corda/testing/node/MockNetwork.kt | 4 ++-- .../node/internal/InternalMockNetwork.kt | 3 ++- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/docs/source/api-testing.rst b/docs/source/api-testing.rst index 286e15cb70..78bf5e828f 100644 --- a/docs/source/api-testing.rst +++ b/docs/source/api-testing.rst @@ -201,19 +201,22 @@ CorDapps specific to their role in the network. Running the network ^^^^^^^^^^^^^^^^^^^ +When using a ``MockNetwork``, you must be careful to ensure that all the nodes have processed all the relevant messages +before making assertions about the result of performing some action. For example, if you start a flow to update the ledger +but don't wait until all the nodes involved have processed all the resulting messages, your nodes' vaults may not be in +the state you expect. -Regular Corda nodes automatically process received messages. When using a ``MockNetwork`` with -``networkSendManuallyPumped`` set to ``false``, you must manually initiate the processing of received messages. - +When ``networkSendManuallyPumped`` is set to ``false``, you must manually initiate the processing of received messages. You manually process received messages as follows: -* ``StartedMockNode.pumpReceive`` to process a single message from the node's queue - -* ``MockNetwork.runNetwork`` to process all the messages in every node's queue. This may generate additional messages - that must in turn be processed - - * ``network.runNetwork(-1)`` (the default in Kotlin) will exchange messages until there are no further messages to +* ``StartedMockNode.pumpReceive()`` processes a single message from the node's queue +* ``MockNetwork.runNetwork()`` processes all the messages in every node's queue until there are no further messages to process + +When ``networkSendManuallyPumped`` is set to ``true``, nodes will automatically process the messages they receive. You +can block until all messages have been processed using ``MockNetwork.waitQuiescent()``. + +.. warning:: If ``threadPerNode`` is set to ``true``, ``networkSendManuallyPumped`` must also be set to ``true``. Running flows ^^^^^^^^^^^^^ 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 d81fc4c529..61b0198c2a 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 @@ -71,7 +71,7 @@ data class MockNodeParameters constructor( * 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 + * @property networkSendManuallyPumped If false 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. @@ -280,7 +280,7 @@ inline fun > StartedMockNode.registerResponderFlow( * @property cordappPackages A [List] of cordapp packages to scan for any cordapp code, e.g. contract verification code, flows and services. * @property defaultParameters A [MockNetworkParameters] object which contains the same parameters as the constructor, provided * as a convenience for Java users. - * @property networkSendManuallyPumped If true then messages will not be routed from sender to receiver until you use + * @property networkSendManuallyPumped If false 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. 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 454d0e3ca1..3985bf713a 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 @@ -506,7 +506,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe */ @JvmOverloads fun runNetwork(rounds: Int = -1) { - check(!networkSendManuallyPumped) + check(!networkSendManuallyPumped) { "MockNetwork.runNetwork() should only be used when networkSendManuallyPumped == false. " + + "You can use MockNetwork.waitQuiescent() to wait for all the nodes to process all the messages on their queues instead." } fun pumpAll() = messagingNetwork.endpoints.map { it.pumpReceive(false) } if (rounds == -1) { From b7867c3bcbc02163c32ab062700aa2d2d1d77ab7 Mon Sep 17 00:00:00 2001 From: Roger Willis Date: Fri, 17 Aug 2018 13:39:05 +0100 Subject: [PATCH 09/16] * Added "isRelevant" functionality to the vault. (#3789) * * Added "isRelevant" functionality to the vault. * * Changed "isRelevant" to "isParticipant" as this makes more sense in the context of Corda. * Minor tweak to "isParticipant" method in NodeVaultService. * Fixed API break and broken JAva tests. * * Addressed PR comments from Jose. * Changed all mentions of "relevant" and "participant" to "modifiable". * Made the default behaviour of vault queries to return ALL states instead of just MODIFIABLE states as this is what always previously happened. * Updated cash balance queries to only return MODIFIABLE states. * * Updated cash selection and tryLockFungfibleStatesForSpending. --- .../corda/core/node/services/VaultService.kt | 68 +++++++-- .../core/node/services/vault/QueryCriteria.kt | 140 ++++++++++++++---- .../flows/WithReferencedStatesFlowTests.kt | 6 +- .../corda/finance/contracts/GetBalances.kt | 6 +- .../cash/selection/CashSelectionH2Impl.kt | 3 + .../selection/CashSelectionPostgreSQLImpl.kt | 7 +- .../selection/CashSelectionSQLServerImpl.kt | 3 + .../vault/HibernateQueryCriteriaParser.kt | 14 ++ .../node/services/vault/NodeVaultService.kt | 57 +++++-- .../corda/node/services/vault/VaultSchema.kt | 4 + .../services/vault/NodeVaultServiceTest.kt | 49 +++++- 11 files changed, 302 insertions(+), 55 deletions(-) diff --git a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt index 0a9ae5ea86..fb695b7c44 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/VaultService.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.internal.concurrent.doneFuture import net.corda.core.messaging.DataFeed +import net.corda.core.node.services.Vault.StateModificationStatus.* import net.corda.core.node.services.Vault.StateStatus import net.corda.core.node.services.vault.* import net.corda.core.serialization.CordaSerializable @@ -105,6 +106,26 @@ class Vault(val states: Iterable>) { UNCONSUMED, CONSUMED, ALL } + /** + * If the querying node is a participant in a state then it is classed as [MODIFIABLE], although technically the + * state is only _potentially_ modifiable as the contract code may forbid them from performing any actions. + * + * If the querying node is not a participant in a state then it is classed as [NOT_MODIFIABLE]. These types of + * states can still be recorded in the vault if the transaction containing them was recorded with the + * [StatesToRecord.ALL_VISIBLE] flag. This will typically happen for things like reference data which can be + * referenced in transactions as a [ReferencedStateAndRef] but cannot be modified by any party but the maintainer. + * + * If both [MODIFIABLE] and [NOT_MODIFIABLE] states are required to be returned from a query, then the [ALL] flag + * can be used. + * + * NOTE: Default behaviour is for ALL STATES to be returned as this is how Corda behaved before the introduction of + * this query criterion. + */ + @CordaSerializable + enum class StateModificationStatus { + MODIFIABLE, NOT_MODIFIABLE, ALL + } + @CordaSerializable enum class UpdateType { GENERAL, NOTARY_CHANGE, CONTRACT_UPGRADE @@ -131,14 +152,40 @@ class Vault(val states: Iterable>) { val otherResults: List) @CordaSerializable - data class StateMetadata(val ref: StateRef, - val contractStateClassName: String, - val recordedTime: Instant, - val consumedTime: Instant?, - val status: Vault.StateStatus, - val notary: AbstractParty?, - val lockId: String?, - val lockUpdateTime: Instant?) + data class StateMetadata constructor( + val ref: StateRef, + val contractStateClassName: String, + val recordedTime: Instant, + val consumedTime: Instant?, + val status: Vault.StateStatus, + val notary: AbstractParty?, + val lockId: String?, + val lockUpdateTime: Instant?, + val isModifiable: Vault.StateModificationStatus? + ) { + constructor(ref: StateRef, + contractStateClassName: String, + recordedTime: Instant, + consumedTime: Instant?, + status: Vault.StateStatus, + notary: AbstractParty?, + lockId: String?, + lockUpdateTime: Instant? + ) : this(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) + + fun copy( + ref: StateRef = this.ref, + contractStateClassName: String = this.contractStateClassName, + recordedTime: Instant = this.recordedTime, + consumedTime: Instant? = this.consumedTime, + status: Vault.StateStatus = this.status, + notary: AbstractParty? = this.notary, + lockId: String? = this.lockId, + lockUpdateTime: Instant? = this.lockUpdateTime + ): StateMetadata { + return StateMetadata(ref, contractStateClassName, recordedTime, consumedTime, status, notary, lockId, lockUpdateTime, null) + } + } companion object { @Deprecated("No longer used. The vault does not emit empty updates") @@ -181,7 +228,10 @@ interface VaultService { */ @DeleteForDJVM fun whenConsumed(ref: StateRef): CordaFuture> { - val query = QueryCriteria.VaultQueryCriteria(stateRefs = listOf(ref), status = Vault.StateStatus.CONSUMED) + val query = QueryCriteria.VaultQueryCriteria( + stateRefs = listOf(ref), + status = Vault.StateStatus.CONSUMED + ) val result = trackBy(query) val snapshot = result.snapshot.states return if (snapshot.isNotEmpty()) { diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 34c428d6dd..dd6bb54a8e 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -73,6 +73,7 @@ sealed class QueryCriteria : GenericQueryCriteria>? override fun visit(parser: IQueryCriteriaParser): Collection { return parser.parseCriteria(this) @@ -82,51 +83,124 @@ sealed class QueryCriteria : GenericQueryCriteria>? = null, - val stateRefs: List? = null, - val notary: List? = null, - val softLockingCondition: SoftLockingCondition? = null, - val timeCondition: TimeCondition? = null) : CommonQueryCriteria() { + data class VaultQueryCriteria @JvmOverloads constructor( + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + val stateRefs: List? = null, + val notary: List? = null, + val softLockingCondition: SoftLockingCondition? = null, + val timeCondition: TimeCondition? = null, + override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) } + + fun copy( + status: Vault.StateStatus = this.status, + contractStateTypes: Set>? = this.contractStateTypes, + stateRefs: List? = this.stateRefs, + notary: List? = this.notary, + softLockingCondition: SoftLockingCondition? = this.softLockingCondition, + timeCondition: TimeCondition? = this.timeCondition + ): VaultQueryCriteria { + return VaultQueryCriteria( + status, + contractStateTypes, + stateRefs, + notary, + softLockingCondition, + timeCondition + ) + } } /** * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] */ - data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List? = null, - val uuid: List? = null, - val externalId: List? = null, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { - constructor(participants: List? = null, - linearId: List? = null, - status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - contractStateTypes: Set>? = null) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) + data class LinearStateQueryCriteria @JvmOverloads constructor( + val participants: List? = null, + val uuid: List? = null, + val externalId: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + ) : CommonQueryCriteria() { + constructor( + participants: List? = null, + linearId: List? = null, + status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + contractStateTypes: Set>? = null, + isRelevant: Vault.StateModificationStatus + ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes, isRelevant) + + constructor( + participants: List? = null, + linearId: List? = null, + status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + contractStateTypes: Set>? = null + ) : this(participants, linearId?.map { it.id }, linearId?.mapNotNull { it.externalId }, status, contractStateTypes) override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) } + + fun copy( + participants: List? = this.participants, + uuid: List? = this.uuid, + externalId: List? = this.externalId, + status: Vault.StateStatus = this.status, + contractStateTypes: Set>? = this.contractStateTypes + ): LinearStateQueryCriteria { + return LinearStateQueryCriteria( + participants, + uuid, + externalId, + status, + contractStateTypes + ) + } } /** * FungibleStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultFungibleStates] */ - data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List? = null, - val owner: List? = null, - val quantity: ColumnPredicate? = null, - val issuer: List? = null, - val issuerRef: List? = null, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { + data class FungibleAssetQueryCriteria @JvmOverloads constructor( + val participants: List? = null, + val owner: List? = null, + val quantity: ColumnPredicate? = null, + val issuer: List? = null, + val issuerRef: List? = null, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) } + + fun copy( + participants: List? = this.participants, + owner: List? = this.owner, + quantity: ColumnPredicate? = this.quantity, + issuer: List? = this.issuer, + issuerRef: List? = this.issuerRef, + status: Vault.StateStatus = this.status, + contractStateTypes: Set>? = this.contractStateTypes + ): FungibleAssetQueryCriteria { + return FungibleAssetQueryCriteria( + participants, + owner, + quantity, + issuer, + issuerRef, + status, + contractStateTypes + ) + } } /** @@ -137,14 +211,28 @@ sealed class QueryCriteria : GenericQueryCriteria @JvmOverloads constructor - (val expression: CriteriaExpression, - override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, - override val contractStateTypes: Set>? = null) : CommonQueryCriteria() { + data class VaultCustomQueryCriteria @JvmOverloads constructor( + val expression: CriteriaExpression, + override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED, + override val contractStateTypes: Set>? = null, + override val isModifiable: Vault.StateModificationStatus = Vault.StateModificationStatus.ALL + ) : CommonQueryCriteria() { override fun visit(parser: IQueryCriteriaParser): Collection { super.visit(parser) return parser.parseCriteria(this) } + + fun copy( + expression: CriteriaExpression = this.expression, + status: Vault.StateStatus = this.status, + contractStateTypes: Set>? = this.contractStateTypes + ): VaultCustomQueryCriteria { + return VaultCustomQueryCriteria( + expression, + status, + contractStateTypes + ) + } } // timestamps stored in the vault states table [VaultSchema.VaultStates] diff --git a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt index fc9c26b230..b4237b1b93 100644 --- a/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/WithReferencedStatesFlowTests.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.* import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.StatesToRecord +import net.corda.core.node.services.Vault import net.corda.core.node.services.queryBy import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.transactions.LedgerTransaction @@ -102,7 +103,10 @@ internal class UseRefState(val linearId: UniqueIdentifier) : FlowLogic(query).states.single() return subFlow(FinalityFlow( transaction = serviceHub.signInitialTransaction(TransactionBuilder(notary = notary).apply { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt index ba31fabc5b..b1ff47295a 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/GetBalances.kt @@ -21,7 +21,8 @@ private fun generateCashSumCriteria(currency: Currency): QueryCriteria { val sumCriteria = QueryCriteria.VaultCustomQueryCriteria(sum) val ccyIndex = builder { CashSchemaV1.PersistentCashState::currency.equal(currency.currencyCode) } - val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex) + // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). + val ccyCriteria = QueryCriteria.VaultCustomQueryCriteria(ccyIndex, isModifiable = Vault.StateModificationStatus.MODIFIABLE) return sumCriteria.and(ccyCriteria) } @@ -30,7 +31,8 @@ private fun generateCashSumsCriteria(): QueryCriteria { CashSchemaV1.PersistentCashState::pennies.sum(groupByColumns = listOf(CashSchemaV1.PersistentCashState::currency), orderBy = Sort.Direction.DESC) } - return QueryCriteria.VaultCustomQueryCriteria(sum) + // This query should only return cash states the calling node is a participant of (meaning they can be modified/spent). + return QueryCriteria.VaultCustomQueryCriteria(sum, isModifiable = Vault.StateModificationStatus.MODIFIABLE) } private fun rowsToAmount(currency: Currency, rows: Vault.Page>): Amount { diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt index bff90c053f..b59be03fb3 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionH2Impl.kt @@ -33,11 +33,14 @@ class CashSelectionH2Impl : AbstractCashSelection() { override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { connection.createStatement().use { it.execute("CALL SET(@t, CAST(0 AS BIGINT));") } + // state_status = 0 -> UNCONSUMED. + // is_modifiable = 0 -> MODIFIABLE. val selectJoin = """ SELECT vs.transaction_id, vs.output_index, ccs.pennies, SET(@t, ifnull(@t,0)+ccs.pennies) total_pennies, vs.lock_id FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 + AND vs.is_modifiable = 0 AND ccs.ccy_code = ? and @t < ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt index 497336acbd..b08ddfbbfc 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionPostgreSQLImpl.kt @@ -4,7 +4,9 @@ import net.corda.core.contracts.Amount import net.corda.core.crypto.toStringShort import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party -import net.corda.core.utilities.* +import net.corda.core.utilities.OpaqueBytes +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug import java.sql.Connection import java.sql.DatabaseMetaData import java.sql.ResultSet @@ -29,6 +31,8 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { // appear in the WHERE clause, hence restricting row selection and adjusting the returned total in the outer query. // 3) Currently (version 9.6), FOR UPDATE cannot be specified with window functions override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { + // state_status = 0 -> UNCONSUMED. + // is_modifiable = 0 -> MODIFIABLE. val selectJoin = """SELECT nested.transaction_id, nested.output_index, nested.pennies, nested.total+nested.pennies as total_pennies, nested.lock_id FROM @@ -38,6 +42,7 @@ class CashSelectionPostgreSQLImpl : AbstractCashSelection() { FROM vault_states AS vs, contract_cash_states AS ccs WHERE vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index AND vs.state_status = 0 + AND vs.is_modifiable = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id is null) """ + diff --git a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt index 246842668b..0678c4e6bc 100644 --- a/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt +++ b/finance/src/main/kotlin/net/corda/finance/contracts/asset/cash/selection/CashSelectionSQLServerImpl.kt @@ -42,6 +42,8 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() { // Query plan does index scan on pennies_idx, which may be unavoidable due to the nature of the query. override fun executeQuery(connection: Connection, amount: Amount, lockId: UUID, notary: Party?, onlyFromIssuerParties: Set, withIssuerRefs: Set, withResultSet: (ResultSet) -> Boolean): Boolean { val sb = StringBuilder() + // state_status = 0 -> UNCONSUMED. + // is_modifiable = 0 -> MODIFIABLE. sb.append( """ ;WITH CTE AS ( @@ -56,6 +58,7 @@ class CashSelectionSQLServerImpl : AbstractCashSelection() { ON vs.transaction_id = ccs.transaction_id AND vs.output_index = ccs.output_index WHERE vs.state_status = 0 + vs.is_modifiable = 0 AND ccs.ccy_code = ? AND (vs.lock_id = ? OR vs.lock_id IS NULL) """ diff --git a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt index 66cce97573..a3c6de8d20 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/HibernateQueryCriteriaParser.kt @@ -486,6 +486,20 @@ class HibernateQueryCriteriaParser(val contractStateType: Class(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable)) + } + } else { + commonPredicates[predicateID] = criteriaBuilder.equal(vaultStates.get(VaultSchemaV1.VaultStates::isModifiable.name), criteria.isModifiable) + } + } + // contract state types val contractStateTypes = deriveContractStateTypes(criteria.contractStateTypes) if (contractStateTypes.isNotEmpty()) { diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index 1412bcba8e..04d9fdf817 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -18,7 +18,10 @@ import net.corda.node.services.api.SchemaService import net.corda.node.services.api.VaultServiceInternal import net.corda.node.services.schema.PersistentStateService import net.corda.node.services.statemachine.FlowStateMachineImpl -import net.corda.nodeapi.internal.persistence.* +import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit +import net.corda.nodeapi.internal.persistence.currentDBSession +import net.corda.nodeapi.internal.persistence.wrapWithDatabaseTransaction import org.hibernate.Session import rx.Observable import rx.subjects.PublishSubject @@ -104,13 +107,37 @@ class NodeVaultService( val session = currentDBSession() producedStateRefsMap.forEach { stateAndRef -> - val state = VaultSchemaV1.VaultStates( + val stateOnly = stateAndRef.value.state.data + // TODO: Optimise this. + // + // For EVERY state to be committed to the vault, this checks whether it is spendable by the recording + // node. The behaviour is as follows: + // + // 1) All vault updates marked as MODIFIABLE will, of, course all have isModifiable = true. + // 2) For ALL_VISIBLE updates, those which are not modifiable will have isModifiable = false. + // + // This is useful when it comes to querying for fungible states, when we do not want non-modifiable states + // included in the result. + // + // The same functionality could be obtained by passing in a list of participants to the vault query, + // however this: + // + // * requires a join on the participants table which results in slow queries + // * states may flip from being non-modifiable to modifiable + // * it's more complicated for CorDapp developers + // + // Adding a new column in the "VaultStates" table was considered the best approach. + val keys = stateOnly.participants.map { it.owningKey } + val isModifiable = isModifiable(stateOnly, keyManagementService.filterMyKeys(keys).toSet()) + val stateToAdd = VaultSchemaV1.VaultStates( notary = stateAndRef.value.state.notary, contractStateClassName = stateAndRef.value.state.data.javaClass.name, stateStatus = Vault.StateStatus.UNCONSUMED, - recordedTime = clock.instant()) - state.stateRef = PersistentStateRef(stateAndRef.key) - session.save(state) + recordedTime = clock.instant(), + isModifiable = if (isModifiable) Vault.StateModificationStatus.MODIFIABLE else Vault.StateModificationStatus.NOT_MODIFIABLE + ) + stateToAdd.stateRef = PersistentStateRef(stateAndRef.key) + session.save(stateToAdd) } consumedStateRefs.forEach { stateRef -> val state = session.get(VaultSchemaV1.VaultStates::class.java, PersistentStateRef(stateRef)) @@ -161,7 +188,7 @@ class NodeVaultService( val ourNewStates = when (statesToRecord) { StatesToRecord.NONE -> throw AssertionError("Should not reach here") StatesToRecord.ONLY_RELEVANT -> tx.outputs.withIndex().filter { - isRelevant(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet()) + isModifiable(it.value.data, keyManagementService.filterMyKeys(tx.outputs.flatMap { it.data.participants.map { it.owningKey } }).toSet()) } StatesToRecord.ALL_VISIBLE -> tx.outputs.withIndex() }.map { tx.outRef(it.index) } @@ -190,7 +217,7 @@ class NodeVaultService( val myKeys by lazy { keyManagementService.filterMyKeys(ltx.outputs.flatMap { it.data.participants.map { it.owningKey } }) } val (consumedStateAndRefs, producedStates) = ltx.inputs.zip(ltx.outputs).filter { (_, output) -> if (statesToRecord == StatesToRecord.ONLY_RELEVANT) { - isRelevant(output.data, myKeys.toSet()) + isModifiable(output.data, myKeys.toSet()) } else { true } @@ -368,12 +395,15 @@ class NodeVaultService( return emptyList() } - // Enrich QueryCriteria with additional default attributes (such as soft locks) + // Enrich QueryCriteria with additional default attributes (such as soft locks). + // We only want to return MODIFIABLE states here. val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF) val sorter = Sort(setOf(Sort.SortColumn(sortAttribute, Sort.Direction.ASC))) val enrichedCriteria = QueryCriteria.VaultQueryCriteria( contractStateTypes = setOf(contractStateType), - softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId))) + softLockingCondition = QueryCriteria.SoftLockingCondition(QueryCriteria.SoftLockingType.UNLOCKED_AND_SPECIFIED, listOf(lockId)), + isModifiable = Vault.StateModificationStatus.MODIFIABLE + ) val results = queryBy(contractStateType, enrichedCriteria.and(eligibleStatesQuery), sorter) var claimedAmount = 0L @@ -396,9 +426,11 @@ class NodeVaultService( } @VisibleForTesting - internal fun isRelevant(state: ContractState, myKeys: Set): Boolean { + internal fun isModifiable(state: ContractState, myKeys: Set): Boolean { val keysToCheck = when (state) { - is OwnableState -> listOf(state.owner.owningKey) + // Sometimes developers forget to add the owning key to participants for OwnableStates. + // TODO: This logic should probably be moved to OwnableState so we can just do a simple intersection here. + is OwnableState -> (state.participants.map { it.owningKey } + state.owner.owningKey).toSet() else -> state.participants.map { it.owningKey } } return keysToCheck.any { it in myKeys } @@ -477,7 +509,8 @@ class NodeVaultService( vaultState.stateStatus, vaultState.notary, vaultState.lockId, - vaultState.lockUpdateTime)) + vaultState.lockUpdateTime, + vaultState.isModifiable)) } else { // TODO: improve typing of returned other results log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" } diff --git a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt index a1c7a7f8e8..56897b3087 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/VaultSchema.kt @@ -57,6 +57,10 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio @Column(name = "lock_id", nullable = true) var lockId: String? = null, + /** Used to determine whether a state is modifiable by the recording node */ + @Column(name = "is_modifiable", nullable = false) + var isModifiable: Vault.StateModificationStatus, + /** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */ @Column(name = "lock_timestamp", nullable = true) var lockUpdateTime: Instant? = null 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 d0506cfe07..d652aeb62e 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 @@ -28,6 +28,8 @@ import net.corda.finance.utils.sumCash import net.corda.node.services.api.IdentityServiceInternal import net.corda.node.services.api.WritableTransactionStorage import net.corda.nodeapi.internal.persistence.CordaPersistence +import net.corda.testing.contracts.DummyContract +import net.corda.testing.contracts.DummyState import net.corda.testing.core.* import net.corda.testing.internal.LogHelper import net.corda.testing.internal.rigorousMock @@ -51,7 +53,7 @@ import kotlin.test.assertTrue class NodeVaultServiceTest { private companion object { - val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName) + val cordappPackages = listOf("net.corda.finance.contracts.asset", CashSchemaV1::class.packageName, "net.corda.testing.contracts") val dummyCashIssuer = TestIdentity(CordaX500Name("Snake Oil Issuer", "London", "GB"), 10) val DUMMY_CASH_ISSUER = dummyCashIssuer.ref(1) val bankOfCorda = TestIdentity(BOC_NAME) @@ -526,17 +528,17 @@ class NodeVaultServiceTest { val amount = Amount(1000, Issued(BOC.ref(1), GBP)) val wellKnownCash = Cash.State(amount, identity.party) val myKeys = services.keyManagementService.filterMyKeys(listOf(wellKnownCash.owner.owningKey)) - assertTrue { service.isRelevant(wellKnownCash, myKeys.toSet()) } + assertTrue { service.isModifiable(wellKnownCash, myKeys.toSet()) } val anonymousIdentity = services.keyManagementService.freshKeyAndCert(identity, false) val anonymousCash = Cash.State(amount, anonymousIdentity.party) val anonymousKeys = services.keyManagementService.filterMyKeys(listOf(anonymousCash.owner.owningKey)) - assertTrue { service.isRelevant(anonymousCash, anonymousKeys.toSet()) } + assertTrue { service.isModifiable(anonymousCash, anonymousKeys.toSet()) } val thirdPartyIdentity = AnonymousParty(generateKeyPair().public) val thirdPartyCash = Cash.State(amount, thirdPartyIdentity) val thirdPartyKeys = services.keyManagementService.filterMyKeys(listOf(thirdPartyCash.owner.owningKey)) - assertFalse { service.isRelevant(thirdPartyCash, thirdPartyKeys.toSet()) } + assertFalse { service.isModifiable(thirdPartyCash, thirdPartyKeys.toSet()) } } // TODO: Unit test linear state relevancy checks @@ -727,4 +729,43 @@ class NodeVaultServiceTest { } assertThat(recordedStates).isEqualTo(coins.size) } + + @Test + fun `test state relevance criteria`() { + fun createTx(number: Int, vararg participants: Party): SignedTransaction { + return services.signInitialTransaction(TransactionBuilder(DUMMY_NOTARY).apply { + addOutputState(DummyState(number, participants.toList()), DummyContract.PROGRAM_ID) + addCommand(DummyCommandData, listOf(megaCorp.publicKey)) + }) + } + + fun List>.getNumbers() = map { it.state.data.magicNumber }.toSet() + + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(1, megaCorp.party))) + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(2, miniCorp.party))) + services.recordTransactions(StatesToRecord.ONLY_RELEVANT, listOf(createTx(3, miniCorp.party, megaCorp.party))) + services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(4, miniCorp.party))) + services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(5, bankOfCorda.party))) + services.recordTransactions(StatesToRecord.ALL_VISIBLE, listOf(createTx(6, megaCorp.party, bankOfCorda.party))) + services.recordTransactions(StatesToRecord.NONE, listOf(createTx(7, bankOfCorda.party))) + + // Test one. + // StateModificationStatus is MODIFIABLE by default. This should return two states. + val resultOne = vaultService.queryBy().states.getNumbers() + assertEquals(setOf(1, 3, 4, 5, 6), resultOne) + + // Test two. + // StateModificationStatus set to NOT_MODIFIABLE. + val criteriaTwo = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.NOT_MODIFIABLE) + val resultTwo = vaultService.queryBy(criteriaTwo).states.getNumbers() + assertEquals(setOf(4, 5), resultTwo) + + // Test three. + // StateModificationStatus set to ALL. + val criteriaThree = VaultQueryCriteria(isModifiable = Vault.StateModificationStatus.MODIFIABLE) + val resultThree = vaultService.queryBy(criteriaThree).states.getNumbers() + assertEquals(setOf(1, 3, 6), resultThree) + + // We should never see 2 or 7. + } } From 911aa1381beddf9edbddb50118ddbcc738b6e553 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 17 Aug 2018 14:08:48 +0100 Subject: [PATCH 10/16] CORDA-1906: Add Servlet 3.1 implementation into the WebServer. (#3794) --- webserver/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webserver/build.gradle b/webserver/build.gradle index 93f3d85019..2336b9a756 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -47,7 +47,7 @@ dependencies { compile "org.eclipse.jetty:jetty-continuation:${jetty_version}" compile "org.glassfish.jersey.core:jersey-server:$jersey_version" - compile "org.glassfish.jersey.containers:jersey-container-servlet-core:$jersey_version" + compile "org.glassfish.jersey.containers:jersey-container-servlet:$jersey_version" compile "org.glassfish.jersey.containers:jersey-container-jetty-http:$jersey_version" compile "org.glassfish.jersey.media:jersey-media-json-jackson:$jersey_version" From 5d39f2bb46451dfa7bf40030d9272c48b4d6acc1 Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 17 Aug 2018 16:52:56 +0100 Subject: [PATCH 11/16] Better docs of CorDapp structure and node interaction (#3761) * Clean-up. Instructions on how template would be modified for production. * Change page titles to make it clearer make they contain. * Simple example of how to connect to node via RPC. Explanation of how to interact with node via RPC. * Bigger warning about deprecated webserver. Makes it clear that CordaRPCClient is THE way to interact with a node. * Review from Clinton. * Separating template info from general info. --- docs/source/clientrpc.rst | 53 +++++-- docs/source/corda-configuration-file.rst | 2 +- docs/source/cordapp-build-systems.rst | 4 +- docs/source/cordapp-overview.rst | 34 ++++- .../net/corda/docs/ClientRpcExampleJava.java | 34 +++++ .../kotlin/net/corda/docs/ClientRpcExample.kt | 31 ++++ docs/source/shell.rst | 4 +- docs/source/tutorial-clientrpc-api.rst | 6 + docs/source/writing-a-cordapp.rst | 142 +++++++++--------- 9 files changed, 213 insertions(+), 97 deletions(-) create mode 100644 docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java create mode 100644 docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 546744e8a6..a2db1469a9 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -1,27 +1,52 @@ -Client RPC -========== +.. highlight:: kotlin +.. raw:: html + + + + +Interacting with a node +======================= .. contents:: Overview -------- -Corda provides a client library that allows you to easily write clients in a JVM-compatible language to interact -with a running node. The library connects to the node using a message queue protocol and then provides a simple RPC -interface to interact with the node. You make calls on a Java object as normal, and the marshalling back and forth is -handled for you. +You should interact with your node using the `CordaRPCClient`_ library. This library that allows you to easily +write clients in a JVM-compatible language to interact with a running node. The library connects to the node using a +message queue protocol and then provides a simple RPC interface to interact with the node. You make calls on a JVM +object as normal, and the marshalling back and forth is handled for you. -The starting point for the client library is the `CordaRPCClient`_ class. `CordaRPCClient`_ provides a ``start`` method -that returns a `CordaRPCConnection`_. A `CordaRPCConnection`_ allows you to access an implementation of the -`CordaRPCOps`_ interface with ``proxy`` in Kotlin or ``getProxy()`` in Java. The observables that are returned by RPC -operations can be subscribed to in order to receive an ongoing stream of updates from the node. More detail on this -functionality is provided in the docs for the ``proxy`` method. +.. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with + your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node + and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this + `here `_. + +Connecting to a node via RPC +---------------------------- +`CordaRPCClient`_ provides a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_. +`CordaRPCConnection`_ provides a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_ +object that you can use to interact with the node. + +Here is an example of using `CordaRPCClient`_ to connect to a node and log the current time on its internal clock: + +.. container:: codeset + + .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt + :language: kotlin + :start-after: START 1 + :end-before: END 1 + + .. literalinclude:: example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java + :language: java + :start-after: START 1 + :end-before: END 1 .. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use`` method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create a new proxy for every call you make - reuse an existing one. -For a brief tutorial on using the RPC API, see :doc:`tutorial-clientrpc-api`. +For further information on using the RPC API, see :doc:`tutorial-clientrpc-api`. RPC permissions --------------- @@ -276,7 +301,7 @@ will be freed automatically. is non-deterministic. .. note:: Observables can only be used as return arguments of an RPC call. It is not currently possible to pass -Observables as parameters to the RPC methods. + Observables as parameters to the RPC methods. Futures ------- @@ -306,7 +331,7 @@ side as if it was thrown from inside the called RPC method. These exceptions can Connection management --------------------- -It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCCLient.start()`` +It is possible to not be able to connect to the server on the first attempt. In that case, the ``CordaRPCClient.start()`` method will throw an exception. The following code snippet is an example of how to write a simple retry mechanism for such situations: diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index dfef436e7a..d21076eed4 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -1,4 +1,4 @@ -Node configuration +Configuring a node ================== .. contents:: diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index 1548cf0085..7d3a4a8cb7 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -1,5 +1,5 @@ -Building a CorDapp -================== +Building and installing a CorDapp +================================= .. contents:: diff --git a/docs/source/cordapp-overview.rst b/docs/source/cordapp-overview.rst index 36c845036a..7c59ed6701 100644 --- a/docs/source/cordapp-overview.rst +++ b/docs/source/cordapp-overview.rst @@ -3,20 +3,38 @@ What is a CorDapp? CorDapps (Corda Distributed Applications) are distributed applications that run on the Corda platform. The goal of a CorDapp is to allow nodes to reach agreement on updates to the ledger. They achieve this goal by defining flows that -Corda node owners can invoke through RPC calls: +Corda node owners can invoke over RPC: .. image:: resources/node-diagram.png + :scale: 25% + :align: center -CorDapps are made up of the following key components: +CorDapp components +------------------ +CorDapps take the form of a set of JAR files containing class definitions written in Java and/or Kotlin. -* States, defining the facts over which agreement is reached (see :doc:`Key Concepts - States `) +These class definitions will commonly include the following elements: + +* Flows: Define a routine for the node to run, usually to update the ledger + (see :doc:`Key Concepts - Flows `). They subclass ``FlowLogic`` +* States: Define the facts over which agreement is reached (see :doc:`Key Concepts - States `). + They implement the ``ContractState`` interface * Contracts, defining what constitutes a valid ledger update (see - :doc:`Key Concepts - Contracts `) -* Services, providing long-lived utilities within the node -* Serialisation whitelists, restricting what types your node will receive off the wire + :doc:`Key Concepts - Contracts `). They implement the ``Contract`` interface +* Services, providing long-lived utilities within the node. They subclass ``SingletonSerializationToken`` +* Serialisation whitelists, restricting what types your node will receive off the wire. They implement the + ``SerializationWhitelist`` interface -Each CorDapp is installed at the level of the individual node, rather than on the network itself. For example, a node -owner may choose to install the Bond Trading CorDapp, with the following components: +But the CorDapp JAR can also include other class definitions. These may include: + +* APIs and static web content: These are served by Corda's built-in webserver. This webserver is not + production-ready, and should be used for testing purposes only +* Utility classes + +An example +---------- +Suppose a node owner wants their node to be able to trade bonds. They may choose to install a Bond Trading CorDapp with +the following components: * A ``BondState``, used to represent bonds as shared facts on the ledger * A ``BondContract``, used to govern which ledger updates involving ``BondState`` states are valid diff --git a/docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java b/docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java new file mode 100644 index 0000000000..2a7bdb4d9b --- /dev/null +++ b/docs/source/example-code/src/main/java/net/corda/docs/ClientRpcExampleJava.java @@ -0,0 +1,34 @@ +package net.corda.docs; + +// START 1 +import net.corda.client.rpc.CordaRPCClient; +import net.corda.client.rpc.CordaRPCConnection; +import net.corda.core.messaging.CordaRPCOps; +import net.corda.core.utilities.NetworkHostAndPort; +import org.apache.activemq.artemis.api.core.ActiveMQException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.ExecutionException; + +class ExampleRpcClientJava { + private static final Logger logger = LoggerFactory.getLogger(ExampleRpcClient.class); + + public static void main(String[] args) throws ActiveMQException, InterruptedException, ExecutionException { + if (args.length != 3) { + throw new IllegalArgumentException("Usage: TemplateClient "); + } + final NetworkHostAndPort nodeAddress = NetworkHostAndPort.parse(args[0]); + String username = args[1]; + String password = args[2]; + + final CordaRPCClient client = new CordaRPCClient(nodeAddress); + final CordaRPCConnection connection = client.start(username, password); + final CordaRPCOps cordaRPCOperations = connection.getProxy(); + + logger.info(cordaRPCOperations.currentNodeTime().toString()); + + connection.notifyServerAndClose(); + } +} +// END 1 \ No newline at end of file diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt new file mode 100644 index 0000000000..e41eb2ed18 --- /dev/null +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcExample.kt @@ -0,0 +1,31 @@ +@file:Suppress("unused") + +package net.corda.docs + +// START 1 +import net.corda.client.rpc.CordaRPCClient +import net.corda.core.utilities.NetworkHostAndPort.Companion.parse +import net.corda.core.utilities.loggerFor +import org.slf4j.Logger + +class ExampleRpcClient { + companion object { + val logger: Logger = loggerFor() + } + + fun main(args: Array) { + require(args.size == 3) { "Usage: TemplateClient " } + val nodeAddress = parse(args[0]) + val username = args[1] + val password = args[2] + + val client = CordaRPCClient(nodeAddress) + val connection = client.start(username, password) + val cordaRPCOperations = connection.proxy + + logger.info(cordaRPCOperations.currentNodeTime().toString()) + + connection.notifyServerAndClose() + } +} +// END 1 \ No newline at end of file diff --git a/docs/source/shell.rst b/docs/source/shell.rst index aa824f3e76..73ef8f0a6d 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -4,8 +4,8 @@ -Shell -===== +Node shell +========== .. contents:: diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index ff6739b70a..597e8a96f9 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -1,3 +1,9 @@ +.. highlight:: kotlin +.. raw:: html + + + + .. _graphstream: http://graphstream-project.org/ Using the client RPC API diff --git a/docs/source/writing-a-cordapp.rst b/docs/source/writing-a-cordapp.rst index bda4f2977a..02bca02c2b 100644 --- a/docs/source/writing-a-cordapp.rst +++ b/docs/source/writing-a-cordapp.rst @@ -1,58 +1,70 @@ -Writing a CorDapp +CorDapp structure ================= .. contents:: -Overview --------- -CorDapps can be written in either Java, Kotlin, or a combination of the two. Each CorDapp component takes the form -of a JVM class that subclasses or implements a Corda library type: - -* Flows subclass ``FlowLogic`` -* States implement ``ContractState`` -* Contracts implement ``Contract`` -* Services subclass ``SingletonSerializationToken`` -* Serialisation whitelists implement ``SerializationWhitelist`` - -Web content and RPC clients ---------------------------- -For testing purposes, CorDapps may also include: - -* **APIs and static web content**: These are served by Corda's built-in webserver. This webserver is not - production-ready, and should be used for testing purposes only - -* **RPC clients**: These are programs that automate the process of interacting with a node via RPC - -In production, a production-ready webserver should be used, and these files should be moved into a different module or -project so that they do not bloat the CorDapp at build time. - .. _cordapp-structure: -Structure and dependencies --------------------------- -You should base your project on the Java template (for CorDapps written in Java) or the Kotlin template (for CorDapps -written in Kotlin): +Modules +------- +The source code for a CorDapp is divided into one or more modules, each of which will be compiled into a separate JAR. +Together, these JARs represent a single CorDapp. Typically, a Cordapp contains all the classes required for it to be +used standalone. However, some Cordapps are only libraries for other Cordapps and cannot be run standalone. -* `Java Template CorDapp `_ -* `Kotlin Template CorDapp `_ +A common pattern is to have: -Please checkout the branch of the template that corresponds to the version of Corda you are using. For example, someone -building a CorDapp on Corda 3 should use the ``release-V3`` branch of the template. +* One module containing only the CorDapp's contracts and/or states, as well as any required dependencies +* A second module containing the remaining classes that depend on these contracts and/or states -The required dependencies are defined by the ``build.gradle`` file in the root directory of the template. +This is because each time a contract is used in a transaction, the entire JAR containing the contract's definition is +attached to the transaction. This is to ensure that the exact same contract and state definitions are used when +verifying this transaction at a later date. Because of this, you will want to keep this module, and therefore the +resulting JAR file, as small as possible to reduce the size of your transactions and keep your node performant. -The project should be split into two modules: +However, this two-module structure is not prescriptive: -* A ``cordapp-contracts-states`` module containing classes such as contracts and states that will be sent across the - wire as part of a flow -* A ``cordapp`` module containing the remaining classes +* A library CorDapp containing only contracts and states would only need a single module -Each module will be compiled into its own CorDapp. This minimises the size of the JAR that has to be sent across the -wire when nodes are agreeing ledger updates. +* In a CorDapp with multiple sets of contracts and states that **do not** depend on each other, each independent set of + contracts and states would go in a separate module to reduce transaction size + +* In a CorDapp with multiple sets of contracts and states that **do** depend on each other, either keep them in the + same module or create separate modules that depend on each other + +* The module containing the flows and other classes can be structured in any way because it is not attached to + transactions + +Template CorDapps +----------------- +You should base your project on one of the following templates: + +* `Java Template CorDapp `_ (for CorDapps written in Java) +* `Kotlin Template CorDapp `_ (for CorDapps written in Kotlin) + +Please use the branch of the template that corresponds to the major version of Corda you are using. For example, +someone building a CorDapp on Corda 3.2 should use the ``release-V3`` branch of the template. + +Build system +^^^^^^^^^^^^ + +The templates are built using Gradle. A Gradle wrapper is provided in the ``wrapper`` folder, and the dependencies are +defined in the ``build.gradle`` files. See :doc:`cordapp-build-systems` for more information. + +No templates are currently provided for Maven or other build systems. + +Modules +^^^^^^^ +The templates are split into two modules: + +* A ``cordapp-contracts-states`` module containing the contracts and states +* A ``cordapp`` module containing the remaining classes that depends on the ``cordapp-contracts-states`` module + +These modules will be compiled into two JARs - a ``cordapp-contracts-states`` JAR and a ``cordapp`` JAR - which +together represent the Template CorDapp. Module one - cordapp-contracts-states -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Here is the structure of the ``src`` directory for the ``cordapp-contracts-states`` module of the Java template: .. parsed-literal:: @@ -73,8 +85,8 @@ These are definitions for classes that we expect to have to send over the wire. CorDapp. Module two - cordapp -^^^^^^^^^^^^^^^^^^^^ -Here is the structure of the ``src`` directory for the ``cordapp`` module: +~~~~~~~~~~~~~~~~~~~~ +Here is the structure of the ``src`` directory for the ``cordapp`` module of the Java template: .. parsed-literal:: @@ -116,37 +128,27 @@ The ``src`` directory is structured as follows: Within ``main``, we have the following directories: -* ``resources/META-INF/services`` contains registries of the CorDapp's serialisation whitelists and web plugins -* ``resources/certificates`` contains dummy certificates for test purposes -* ``resources/templateWeb`` contains a dummy front-end -* ``java`` (or ``kotlin`` in the Kotlin template), which includes the source-code for our CorDapp +* ``java``, which contains the source-code for our CorDapp: -The source-code for our CorDapp breaks down as follows: + * ``TemplateFlow.java``, which contains a template ``FlowLogic`` subclass + * ``TemplateState.java``, which contains a template ``ContractState`` implementation + * ``TemplateContract.java``, which contains a template ``Contract`` implementation + * ``TemplateSerializationWhitelist.java``, which contains a template ``SerializationWhitelist`` implementation + * ``TemplateApi.java``, which contains a template API for the deprecated Corda webserver + * ``TemplateWebPlugin.java``, which registers the API and front-end for the deprecated Corda webserver + * ``TemplateClient.java``, which contains a template RPC client for interacting with our CorDapp -* ``TemplateFlow.java``, which contains a dummy ``FlowLogic`` subclass -* ``TemplateState.java``, which contains a dummy ``ContractState`` implementation -* ``TemplateContract.java``, which contains a dummy ``Contract`` implementation -* ``TemplateSerializationWhitelist.java``, which contains a dummy ``SerializationWhitelist`` implementation +* ``resources/META-INF/services``, which contains various registries: -In developing your CorDapp, you should start by modifying these classes to define the components of your CorDapp. A -single CorDapp can define multiple flows, states, and contracts. + * ``net.corda.core.serialization.SerializationWhitelist``, which registers the CorDapp's serialisation whitelists + * ``net.corda.webserver.services.WebServerPluginRegistry``, which registers the CorDapp's web plugins -The template also includes a web API and RPC client: +* ``resources/templateWeb``, which contains a template front-end -* ``TemplateApi.java`` -* ``TemplateClient.java`` -* ``TemplateWebPlugin.java`` +In a production CorDapp: -These are for testing purposes and would be removed in a production CorDapp. +* We would remove the files related to the deprecated Corda webserver (``TemplateApi.java``, + ``TemplateWebPlugin.java``, ``resources/templateWeb``, and ``net.corda.webserver.services.WebServerPluginRegistry``) + and replace them with a production-ready webserver -Resources ---------- -In writing a CorDapp, these pages may be particularly helpful: - -* :doc:`getting-set-up`, to set up your development environment. -* The :doc:`hello-world-introduction` tutorial to write your first CorDapp. -* :doc:`cordapp-build-systems` to build and run your CorDapp. -* The `API docs `_ to read about the API available in developing CorDapps. -* There is also a :doc:`cheat-sheet` recapping the key types. -* The :doc:`flow-cookbook` to see code examples of how to perform common flow tasks. -* `Sample CorDapps `_ showing various parts of Corda's functionality. \ No newline at end of file +* We would also move ``TemplateClient.java`` into a separate module so that it is not included in the CorDapp \ No newline at end of file From 4634283665a046d12f12522c31e3a9afb1ef439a Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Fri, 17 Aug 2018 16:59:52 +0100 Subject: [PATCH 12/16] Azure OS testnet instructions - minor changes (#3777) * Formatting. * Further formatting. * Updates Azure instructions for OS testnet nodes. --- docs/source/azure-vm-explore.rst | 119 +++++++++++++++---------------- 1 file changed, 58 insertions(+), 61 deletions(-) diff --git a/docs/source/azure-vm-explore.rst b/docs/source/azure-vm-explore.rst index cd0c627725..4c206fa8b6 100644 --- a/docs/source/azure-vm-explore.rst +++ b/docs/source/azure-vm-explore.rst @@ -3,23 +3,23 @@ Deploying Corda to Corda Testnet from an Azure Cloud Platform VM .. contents:: -This document explains how to deploy a Corda node to Azure that can connect directly to the Corda Testnet. A self service download link can be obtained from https://testnet.corda.network. This document will describe how to set up a virtual machine on the Azure Cloud Platform to deploy your pre-configured Corda node and automatically connnect to Testnet. +This document will describe how to set up a virtual machine on the Azure Cloud Platform to deploy your pre-configured +Corda node and automatically connnect to Testnet. A self-service download link can be obtained from +https://testnet.corda.network. Pre-requisites -------------- -* Ensure you have a registered Microsoft Azure account which can create virtual machines and you are logged on to the Azure portal: https://portal.azure.com. - +* Ensure you have a registered Microsoft Azure account which can create virtual machines. Deploy Corda node ----------------- Browse to https://portal.azure.com and log in with your Microsoft account. +STEP 1: Create a Resource Group +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**STEP 1: Create a Resource Group** - -Click on the "Resource groups" link in the side nav in the Azure -Portal and then click "Add": +Click on the "Resource groups" link in the side nav in the Azure Portal and then click "Add": .. image:: resources/azure-rg.png @@ -27,109 +27,101 @@ Fill in the form and click "Create": .. image:: resources/azure-rg-2.png -**STEP 2: Launch the VM** +STEP 2: Launch the VM +~~~~~~~~~~~~~~~~~~~~~ At the top of the left sidenav click on the button with the green cross "Create a resource". -In this example we are going to use an Ubuntu server so select the latest Ubuntu Server option. +In this example we are going to use an Ubuntu server so select the latest Ubuntu Server option: .. image:: resources/azure-select-ubuntu.png Fill in the form: +* Add a username (to log into the VM) and choose and enter a password +* Choose the resource group we created earlier from the "Use existing" dropdown +* Select a cloud region geographically near to your location to host your VM + +Click on "OK": + .. image:: resources/azure-vm-form.png -Add a username (to log into the VM) and choose and enter a password. - -Choose the resource group we created earlier from the "Use existing" dropdown. - -Select a cloud region geographically near to your location to host your VM. - -Click on OK. - -Choose the "D4S_V3 Standard" option and click "Select": +Choose a size ("D4S_V3 Standard" is recommended if available) and click "Select": .. image:: resources/azure-instance-type.png -Click on "Public IP address" to open the settings panel +Click on "Public IP address" to open the "Settings" panel .. image:: resources/azure-vm-settings.png -Set the IP address to "Static" under Assignment. +Set the IP address to "Static" under "Assignment" and click "OK": -.. note:: This is so the IP address for your node does not change rapidly in the global network map. +.. note:: This is so the IP address for your node does not change frequently in the global network map. .. image:: resources/azure-set-static-ip.png -Click OK. - -Next click on "Network security group (firewall)": +Next toggle "Network Security Group" to advanced and click on "Network security group (firewall)": .. image:: resources/azure-nsg.png -Add inbound rules for ports 8080 (webserver), and -10002-10003 for the P2P and RPC ports used by the Corda node -respectively: - -Add 3 rules with the following port, name and priorities: +Add the following inbound rules for ports 8080 (webserver), and 10002-10003 for the P2P and RPC ports used by the Corda +node respectively: .. code:: bash - Port range: 10002, Priority: 1041 Name: Port_10002 - Port range: 10003, Priority: 1042 Name: Port_10003 - Port range: 8080, Priority: 1043 Name: Port_8080 + Destination port ranges: 10002, Priority: 1041 Name: Port_10002 + Destination port ranges: 10003, Priority: 1042 Name: Port_10003 + Destination port ranges: 8080, Priority: 1043 Name: Port_8080 + Destination port ranges: 22, Priority: 1044 Name: Port_22 + +.. note:: The priority has to be unique number in the range 900 (highest) and 4096 (lowest) priority. Make sure each + rule has a unique priority or there will be a validation failure and error message. -.. note:: The priority has to be unique number in the range 900 - (highest) and 4096 (lowest) priority. Make sure each rule - has a unique priority or there will be a validation failure and error message. - .. image:: resources/azure-nsg-2.png -Click OK and OK again on the Settings panel. +Click "OK" and "OK" again on the "Settings" panel: .. image:: resources/azure-settings-ok.png - -Click "Create" and wait a few minutes for your instance to provision -and start running. +Click "Create" and wait a few minutes for your instance to be provisioned and start running: .. image:: resources/azure-create-vm.png - - -**STEP 3: Connect to your VM and set up the environment** +STEP 3: Connect to your VM and set up the environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once your instance is running click on the "Connect" button and copy the ssh command: .. image:: resources/azure-ssh.png -Enter the ssh command into your terminal. At the prompt to continue connecting type yes and then enter the password you configured earlier to log into the remote VM: +Enter the ssh command into your terminal. At the prompt, type "yes" to continue connecting and then enter the password +you configured earlier to log into the remote VM: .. image:: resources/azure-shell.png +STEP 4: Download and set up your Corda node +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -**STEP 4: Download and set up your Corda node** +Now that your Azure environment is configured you can switch to the +`Testnet web application `_ and click "Copy" to get a one-time installation +script. -Now your Azure environment is configured you can switch to the Testnet -web application and click on the copy to clipboard button to get a one -time installation script. - -.. note:: If you have not already set up your account on Testnet then please visit https://testnet.corda.network and sign up. +.. note:: If you have not already set up your account on Testnet, please visit https://testnet.corda.network and sign + up. +.. note:: You can generate as many Testnet identites as you like by refreshing this page to generate a new one-time + link. .. image:: resources/testnet-platform.png - -You can generate as many Testnet identites as you like by refreshing -this page to generate a new one time link. -In the terminal of your cloud instance paste the command you just copied to install and run -your unique Corda node: +In the terminal of your cloud instance, paste the command you just copied to install and run your Corda node: .. code:: bash sudo ONE_TIME_DOWNLOAD_KEY=YOUR_UNIQUE_DOWNLOAD_KEY_HERE bash -c "$(curl -L https://testnet.corda.network/api/user/node/install.sh)" -.. warning:: This command will execute the install script as ROOT on your cloud instance. You may wish to examine the script prior to executing it on your machine. +.. warning:: This command will execute the install script as ROOT on your cloud instance. You may wish to examine the + script prior to executing it on your machine. You can follow the progress of the installation by typing the following command in your terminal: @@ -137,15 +129,20 @@ You can follow the progress of the installation by typing the following command tail -f /opt/corda/logs/node-.log -Once the node has booted up you can navigate to the external web address of the instance on port 8080. If everything is working you should see the following: +Once the node has booted up, you can navigate to the external web address of the instance on port 8080: + +.. code:: bash + + http://:8080/ + +If everything is working, you should see the following: .. image:: resources/installed-cordapps.png - Testing your deployment ----------------------- -To test your deployment is working correctly follow the instructions in :doc:`testnet-explorer-corda` to set up the Finance CorDapp and issue cash to a counterparty. - -This will also demonstrate how to install a custom CorDapp. +To test that your deployment is working correctly, follow the instructions in :doc:`testnet-explorer-corda` to set up +the Finance CorDapp and issue cash to a counterparty. +This will also demonstrate how to install a custom CorDapp. \ No newline at end of file From 494661cc0c1d9f4b26a3722ab8a0dec12d99c4d8 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Fri, 17 Aug 2018 17:20:26 +0100 Subject: [PATCH 13/16] CORDA-1905: Extend JSON deserialisation to handle Amount for any T. (#3790) * Extend JSON deserialisation to handle Amount for any T. * Rewrite message for @Deprecated. --- .../corda/client/jackson/JacksonSupport.kt | 4 +- .../client/jackson/internal/CordaModule.kt | 84 ++++++++++++++++-- .../net/corda/client/jackson/AmountTest.kt | 88 +++++++++++++++++++ .../client/jackson/JacksonSupportTest.kt | 8 +- 4 files changed, 175 insertions(+), 9 deletions(-) create mode 100644 client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt index 2820a7d15f..d551889fb1 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/JacksonSupport.kt @@ -413,10 +413,10 @@ object JacksonSupport { } } - @Deprecated("This is an internal class, do not use") + @Deprecated("Do not use - Replaced by Corda's internal AmountDeserializer and TokenDeserializer classes") object AmountDeserializer : JsonDeserializer>() { override fun deserialize(parser: JsonParser, context: DeserializationContext): Amount<*> { - return if (parser.currentToken == JsonToken.VALUE_STRING) { + return if (parser.currentToken() == JsonToken.VALUE_STRING) { Amount.parseCurrency(parser.text) } else { val wrapper = parser.readValueAs() diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt index 32ca33befe..2212638551 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/CordaModule.kt @@ -2,10 +2,9 @@ package net.corda.client.jackson.internal -import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.annotation.* +import com.fasterxml.jackson.annotation.JsonCreator.Mode.* import com.fasterxml.jackson.annotation.JsonInclude.Include -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.annotation.JsonValue import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParseException import com.fasterxml.jackson.core.JsonParser @@ -13,6 +12,9 @@ import com.fasterxml.jackson.core.JsonToken import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier +import com.fasterxml.jackson.databind.deser.ContextualDeserializer +import com.fasterxml.jackson.databind.deser.std.DelegatingDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.databind.node.IntNode import com.fasterxml.jackson.databind.node.ObjectNode @@ -40,15 +42,18 @@ import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.constructorForDeserialization import net.corda.serialization.internal.amqp.hasCordaSerializable import net.corda.serialization.internal.amqp.propertiesForSerialization +import java.math.BigDecimal import java.security.PublicKey import java.security.cert.CertPath import java.time.Instant +import java.util.* class CordaModule : SimpleModule("corda-core") { override fun setupModule(context: SetupContext) { super.setupModule(context) context.addBeanSerializerModifier(CordaSerializableBeanSerializerModifier()) + context.addBeanDeserializerModifier(AmountBeanDeserializerModifier()) context.setMixInAnnotations(PartyAndCertificate::class.java, PartyAndCertificateMixin::class.java) context.setMixInAnnotations(NetworkHostAndPort::class.java, NetworkHostAndPortMixin::class.java) @@ -407,9 +412,78 @@ private interface SecureHashSHA256Mixin @JsonDeserialize(using = JacksonSupport.PublicKeyDeserializer::class) private interface PublicKeyMixin +@Suppress("unused_parameter") @ToStringSerialize -@JsonDeserialize(using = JacksonSupport.AmountDeserializer::class) -private interface AmountMixin +private abstract class AmountMixin @JsonCreator(mode = DISABLED) constructor( + quantity: Long, + displayTokenSize: BigDecimal, + token: Any +) { + /** + * This mirrors the [Amount] constructor that we want Jackson to use, and + * requires that we also tell Jackson NOT to use [Amount]'s primary constructor. + */ + @JsonCreator constructor( + @JsonProperty("quantity") + quantity: Long, + + @JsonDeserialize(using = TokenDeserializer::class) + @JsonProperty("token") + token: Any + ) : this(quantity, Amount.getDisplayTokenSize(token), token) +} + +/** + * Implements polymorphic deserialization for [Amount.token]. Kotlin must + * be able to determine the concrete [Amount] type at runtime, or it will + * fall back to using [Currency]. + */ +private class TokenDeserializer(private val tokenType: Class<*>) : JsonDeserializer(), ContextualDeserializer { + @Suppress("unused") + constructor() : this(Currency::class.java) + + override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any = parser.readValueAs(tokenType) + + override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): TokenDeserializer { + if (property == null) return this + return TokenDeserializer(property.type.rawClass.let { type -> + if (type == Any::class.java) Currency::class.java else type + }) + } +} + +/** + * Intercepts bean-based deserialization for the generic [Amount] type. + */ +private class AmountBeanDeserializerModifier : BeanDeserializerModifier() { + override fun modifyDeserializer(config: DeserializationConfig, description: BeanDescription, deserializer: JsonDeserializer<*>): JsonDeserializer<*> { + val modified = super.modifyDeserializer(config, description, deserializer) + return if (Amount::class.java.isAssignableFrom(description.beanClass)) { + AmountDeserializer(modified) + } else { + modified + } + } +} + +private class AmountDeserializer(delegate: JsonDeserializer<*>) : DelegatingDeserializer(delegate) { + override fun newDelegatingInstance(newDelegatee: JsonDeserializer<*>) = AmountDeserializer(newDelegatee) + + override fun deserialize(parser: JsonParser, context: DeserializationContext?): Any { + return if (parser.currentToken() == JsonToken.VALUE_STRING) { + /* + * This is obviously specific to Amount, and is here to + * preserve the original deserializing behaviour for this case. + */ + Amount.parseCurrency(parser.text) + } else { + /* + * Otherwise continue deserializing our Bean as usual. + */ + _delegatee.deserialize(parser, context) + } + } +} @JsonDeserialize(using = JacksonSupport.OpaqueBytesDeserializer::class) private interface ByteSequenceMixin { diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt new file mode 100644 index 0000000000..967ef28df6 --- /dev/null +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/AmountTest.kt @@ -0,0 +1,88 @@ +package net.corda.client.jackson + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import net.corda.client.jackson.internal.CordaModule +import net.corda.core.contracts.Amount +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.util.* + +class AmountTest { + private companion object { + private val CO2 = CarbonCredit("CO2") + private val jsonMapper: ObjectMapper = ObjectMapper().registerModule(CordaModule()) + private val yamlMapper: ObjectMapper = ObjectMapper(YAMLFactory()).registerModule(CordaModule()) + } + + @Test + fun `Amount(Currency) JSON deserialization`() { + val str = """{ "quantity": 100, "token": "USD" }""" + val amount = jsonMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(100) + assertThat(amount.token).isEqualTo(Currency.getInstance("USD")) + } + + @Test + fun `Amount(Currency) YAML deserialization`() { + val str = """{ quantity: 100, token: USD }""" + val amount = yamlMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(100) + assertThat(amount.token).isEqualTo(Currency.getInstance("USD")) + } + + @Test + fun `Amount(CarbonCredit) JSON deserialization`() { + val str = """{ "quantity": 200, "token": { "type": "CO2" } }""" + val amount = jsonMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(200) + assertThat(amount.token).isEqualTo(CO2) + } + + @Test + fun `Amount(CarbonCredit) YAML deserialization`() { + val str = """{ quantity: 250, token: { type: CO2 } }""" + val amount = yamlMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(250) + assertThat(amount.token).isEqualTo(CO2) + } + + @Test + fun `Amount(Unknown) JSON deserialization`() { + val str = """{ "quantity": 100, "token": "USD" }""" + val amount = jsonMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(100) + assertThat(amount.token).isEqualTo(Currency.getInstance("USD")) + } + + @Test + fun `Amount(Unknown) YAML deserialization`() { + val str = """{ quantity: 100, token: USD }""" + val amount = yamlMapper.readValue>(str, object : TypeReference>() {}) + assertThat(amount.quantity).isEqualTo(100) + assertThat(amount.token).isEqualTo(Currency.getInstance("USD")) + } + + @Test + fun `Amount(Currency) YAML serialization`() { + assertThat(yamlMapper.valueToTree(Amount.parseCurrency("£25000000"))).isEqualTo(TextNode("25000000.00 GBP")) + assertThat(yamlMapper.valueToTree(Amount.parseCurrency("$250000"))).isEqualTo(TextNode("250000.00 USD")) + } + + @Test + fun `Amount(CarbonCredit) JSON serialization`() { + assertThat(jsonMapper.writeValueAsString(Amount(123456, CO2)).trim()) + .isEqualTo(""""123456 CarbonCredit(type=CO2)"""") + } + + @Test + fun `Amount(CarbonCredit) YAML serialization`() { + assertThat(yamlMapper.writeValueAsString(Amount(123456, CO2)).trim()) + .isEqualTo("""--- "123456 CarbonCredit(type=CO2)"""") + } + + data class CarbonCredit(@JsonProperty("type") val type: String) +} \ No newline at end of file diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt index 374009264c..62f69be446 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/JacksonSupportTest.kt @@ -95,10 +95,14 @@ class JacksonSupportTest(@Suppress("unused") private val name: String, factory: @Test fun `Amount(Currency) deserialization`() { val old = mapOf( - "quantity" to 2500000000, - "token" to "USD" + "quantity" to 2500000000, + "token" to "USD" ) assertThat(mapper.convertValue>(old)).isEqualTo(Amount(2_500_000_000, USD)) + } + + @Test + fun `Amount(Currency) Text deserialization`() { assertThat(mapper.convertValue>(TextNode("$25000000"))).isEqualTo(Amount(2_500_000_000, USD)) } From d0ecab07810002942415514201aa8680b1a5d0a7 Mon Sep 17 00:00:00 2001 From: Ivan Schasny Date: Tue, 31 Jul 2018 11:22:28 +0100 Subject: [PATCH 14/16] Added more descriptive error message for the cases when a transaction context is missing from the flow state machine --- .../corda/node/services/statemachine/FlowStateMachineImpl.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 04e73648b3..4cbd80cfd2 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -188,7 +188,9 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, private fun checkDbTransaction(isPresent: Boolean) { if (isPresent) { - requireNotNull(contextTransactionOrNull) + requireNotNull(contextTransactionOrNull) { + "Transaction context is missing. This might happen if a suspendable method is not annotated with @Suspendable annotation." + } } else { require(contextTransactionOrNull == null) } From 88dd6a28884d8d70591994b080899fe5db460ed2 Mon Sep 17 00:00:00 2001 From: Thomas Schroeter Date: Mon, 20 Aug 2018 13:08:58 +0100 Subject: [PATCH 15/16] Fix Jib build (#3811) --- node/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/node/build.gradle b/node/build.gradle index 9d37805131..46f3827a85 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -48,7 +48,7 @@ sourceSets { jib.container { mainClass = "net.corda.node.Corda" args = ['--log-to-console', '--no-local-shell', '--config-file=/config/node.conf'] - jvmFlags = ['-Xmx1g', "-javaagent:/app/libs/quasar-core-${quasar_version}.jar"] + jvmFlags = ['-Xmx1g', '-javaagent:/app/libs/quasar-core-' + "${quasar_version}" + '-jdk8.jar'] } // Use manual resource copying of log4j2.xml rather than source sets. From ff62df8d5a0ab9eabfe919b65a8c73baa3dca7f3 Mon Sep 17 00:00:00 2001 From: Chris Rankin Date: Mon, 20 Aug 2018 13:22:07 +0100 Subject: [PATCH 16/16] CORDA-1907: Allow Corda's shell to deserialise using generic type information. (#3810) --- .ci/api-current.txt | 2 +- .../jackson/StringToMethodCallParser.kt | 8 +- .../client/jackson/internal/JacksonUtils.kt | 5 +- .../jackson/StringToMethodCallParserTest.kt | 49 +++++++++++- .../net/corda/tools/shell/InteractiveShell.kt | 12 +-- .../tools/shell/InteractiveShellJavaTest.java | 74 +++++++++++++++++-- .../corda/tools/shell/InteractiveShellTest.kt | 32 +++++++- 7 files changed, 154 insertions(+), 28 deletions(-) diff --git a/.ci/api-current.txt b/.ci/api-current.txt index 976152d456..b0e292868f 100644 --- a/.ci/api-current.txt +++ b/.ci/api-current.txt @@ -5767,7 +5767,7 @@ public class net.corda.client.jackson.StringToMethodCallParser extends java.lang @NotNull public final net.corda.client.jackson.StringToMethodCallParser$ParsedMethodCall parse(T, String) @NotNull - public final Object[] parseArguments(String, java.util.List>>, String) + public final Object[] parseArguments(String, java.util.List>, String) public static final net.corda.client.jackson.StringToMethodCallParser$Companion Companion ## public static final class net.corda.client.jackson.StringToMethodCallParser$Companion extends java.lang.Object diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt index 9614e770fc..fb4becec14 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/StringToMethodCallParser.kt @@ -10,6 +10,7 @@ import net.corda.core.CordaException import net.corda.core.utilities.contextLogger import java.lang.reflect.Constructor import java.lang.reflect.Method +import java.lang.reflect.Type import java.util.concurrent.Callable import javax.annotation.concurrent.ThreadSafe import kotlin.reflect.KClass @@ -173,7 +174,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( // and fail for that too. for ((index, method) in methods.withIndex()) { try { - val args = parseArguments(name, paramNamesFromMethod(method).zip(method.parameterTypes), argStr) + val args = parseArguments(name, paramNamesFromMethod(method).zip(method.genericParameterTypes), argStr) return ParsedMethodCall(target, method, args) } catch (e: UnparseableCallException) { if (index == methods.size - 1) @@ -189,15 +190,16 @@ open class StringToMethodCallParser @JvmOverloads constructor( * @param methodNameHint A name that will be used in exceptions if thrown; not used for any other purpose. */ @Throws(UnparseableCallException::class) - fun parseArguments(methodNameHint: String, parameters: List>>, args: String): Array { + fun parseArguments(methodNameHint: String, parameters: List>, args: String): Array { // If we have parameters, wrap them in {} to allow the Yaml parser to eat them on a single line. val parameterString = "{ $args }" val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args) if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args) val inOrderParams: List = parameters.mapIndexed { _, (argName, argType) -> val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) + val entryType = om.typeFactory.constructType(argType) try { - om.readValue(entry.traverse(om), argType) + om.readValue(entry.traverse(om), entryType) } catch (e: Exception) { throw UnparseableCallException.FailedParse(e) } diff --git a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt index 255aa85855..30aa1c8d82 100644 --- a/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt +++ b/client/jackson/src/main/kotlin/net/corda/client/jackson/internal/JacksonUtils.kt @@ -3,10 +3,7 @@ package net.corda.client.jackson.internal import com.fasterxml.jackson.annotation.JacksonAnnotationsInside import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.* import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.ser.std.ToStringSerializer import com.fasterxml.jackson.module.kotlin.convertValue diff --git a/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt b/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt index 3eb9ef1042..650c018c48 100644 --- a/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/client/jackson/StringToMethodCallParserTest.kt @@ -3,7 +3,9 @@ package net.corda.client.jackson import net.corda.core.crypto.SecureHash import org.junit.Assert.assertArrayEquals import org.junit.Test +import java.util.* import kotlin.test.assertEquals +import kotlin.test.assertTrue class StringToMethodCallParserTest { @Suppress("UNUSED") @@ -13,6 +15,7 @@ class StringToMethodCallParserTest { fun twoStrings(a: String, b: String) = a + b fun simpleObject(hash: SecureHash.SHA256) = hash.toString() fun complexObject(pair: Pair) = pair + fun complexNestedObject(pairs: Pair>) = pairs fun overload(a: String) = a fun overload(a: String, b: String) = a + b @@ -38,30 +41,68 @@ class StringToMethodCallParserTest { } } + /* + * It would be unreasonable to expect "[ A, B, C ]" to deserialise as "Deque" by default. + * Deque is chosen as we still expect it to preserve the order of its elements. + */ + @Test + fun complexNestedGenericMethod() { + val parser = StringToMethodCallParser(Target::class) + val result = parser.parse(Target(), "complexNestedObject pairs: { first: 101, second: [ A, B, C ] }").invoke() + + assertTrue(result is Pair<*,*>) + result as Pair<*,*> + + assertEquals(101, result.first) + + assertTrue(result.second is Deque<*>) + val deque = result.second as Deque<*> + assertArrayEquals(arrayOf('A', 'B', 'C'), deque.toTypedArray()) + } + @Suppress("UNUSED") class ConstructorTarget(val someWord: String, val aDifferentThing: Int) { constructor(alternativeWord: String) : this(alternativeWord, 0) + constructor(numbers: List) : this(numbers.map(Long::toString).joinToString("+"), numbers.size) } @Test fun ctor1() { val clazz = ConstructorTarget::class.java val parser = StringToMethodCallParser(clazz) - val ctor = clazz.constructors.single { it.parameterCount == 2 } + val ctor = clazz.getDeclaredConstructor(String::class.java, Int::class.java) val names: List = parser.paramNamesFromConstructor(ctor) assertEquals(listOf("someWord", "aDifferentThing"), names) val args: Array = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "someWord: Blah blah blah, aDifferentThing: 12") - assertArrayEquals(args, arrayOf("Blah blah blah", 12)) + assertArrayEquals(arrayOf("Blah blah blah", 12), args) } @Test fun ctor2() { val clazz = ConstructorTarget::class.java val parser = StringToMethodCallParser(clazz) - val ctor = clazz.constructors.single { it.parameterCount == 1 } + val ctor = clazz.getDeclaredConstructor(String::class.java) val names: List = parser.paramNamesFromConstructor(ctor) assertEquals(listOf("alternativeWord"), names) val args: Array = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!") - assertArrayEquals(args, arrayOf("Foo bar!")) + assertArrayEquals(arrayOf("Foo bar!"), args) + } + + @Test + fun constructorWithGenericArgs() { + val clazz = ConstructorTarget::class.java + val ctor = clazz.getDeclaredConstructor(List::class.java) + StringToMethodCallParser(clazz).apply { + val names = paramNamesFromConstructor(ctor) + assertEquals(listOf("numbers"), names) + + val commandLine = "numbers: [ 1, 2, 3 ]" + + val args = parseArguments(clazz.name, names.zip(ctor.parameterTypes), commandLine) + assertArrayEquals(arrayOf(listOf(1, 2, 3)), args) + + val trueArgs = parseArguments(clazz.name, names.zip(ctor.genericParameterTypes), commandLine) + assertArrayEquals(arrayOf(listOf(1L, 2L, 3L)), trueArgs) + } } } diff --git a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt index fc90960866..aa8b9776ac 100644 --- a/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt +++ b/tools/shell/src/main/kotlin/net/corda/tools/shell/InteractiveShell.kt @@ -322,16 +322,16 @@ object InteractiveShell { for (ctor in clazz.constructors) { var paramNamesFromConstructor: List? = null fun getPrototype(): List { - val argTypes = ctor.parameterTypes.map { it.simpleName } + val argTypes = ctor.genericParameterTypes.map { it.typeName } return paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } } try { // Attempt construction with the given arguments. paramNamesFromConstructor = parser.paramNamesFromConstructor(ctor) - val args = parser.parseArguments(clazz.name, paramNamesFromConstructor.zip(ctor.parameterTypes), inputData) - if (args.size != ctor.parameterTypes.size) { - errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.parameterTypes.size} needed)") + val args = parser.parseArguments(clazz.name, paramNamesFromConstructor.zip(ctor.genericParameterTypes), inputData) + if (args.size != ctor.genericParameterTypes.size) { + errors.add("${getPrototype()}: Wrong number of arguments (${args.size} provided, ${ctor.genericParameterTypes.size} needed)") continue } val flow = ctor.newInstance(*args) as FlowLogic<*> @@ -345,10 +345,10 @@ object InteractiveShell { } catch (e: StringToMethodCallParser.UnparseableCallException.TooManyParameters) { errors.add("${getPrototype()}: too many parameters") } catch (e: StringToMethodCallParser.UnparseableCallException.ReflectionDataMissing) { - val argTypes = ctor.parameterTypes.map { it.simpleName } + val argTypes = ctor.genericParameterTypes.map { it.typeName } errors.add("$argTypes: ") } catch (e: StringToMethodCallParser.UnparseableCallException) { - val argTypes = ctor.parameterTypes.map { it.simpleName } + val argTypes = ctor.genericParameterTypes.map { it.typeName } errors.add("$argTypes: ${e.message}") } } diff --git a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java index 498482b057..b3581caf11 100644 --- a/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java +++ b/tools/shell/src/test/java/net/corda/tools/shell/InteractiveShellJavaTest.java @@ -1,10 +1,12 @@ package net.corda.tools.shell; +import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.google.common.collect.Lists; import kotlin.Pair; import net.corda.client.jackson.JacksonSupport; +import net.corda.client.jackson.internal.ToStringSerialize; import net.corda.core.contracts.Amount; import net.corda.core.crypto.SecureHash; import net.corda.core.flows.FlowException; @@ -26,18 +28,20 @@ import rx.Observable; import java.util.*; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; public class InteractiveShellJavaTest { private static TestIdentity megaCorp = new TestIdentity(new CordaX500Name("MegaCorp", "London", "GB")); // should guarantee that FlowA will have synthetic method to access this field - private static String synthetic = "synth"; + private static final String synthetic = "synth"; abstract static class StringFlow extends FlowLogic { abstract String getA(); } + @SuppressWarnings("unused") public static class FlowA extends StringFlow { private String a; @@ -68,6 +72,18 @@ public class InteractiveShellJavaTest { this(party.getName().toString()); } + public FlowA(Integer b, Amount amount) { + this(String.format("%d %s", amount.getQuantity() + (b == null ? 0 : b), amount.getToken())); + } + + public FlowA(String[] b) { + this(String.join("+", b)); + } + + public FlowA(Amount[] amounts) { + this(String.join("++", Arrays.stream(amounts).map(Amount::toString).collect(toList()))); + } + @Nullable @Override public ProgressTracker getProgressTracker() { @@ -75,7 +91,7 @@ public class InteractiveShellJavaTest { } @Override - public String call() throws FlowException { + public String call() { return a; } @@ -106,9 +122,7 @@ public class InteractiveShellJavaTest { FlowSession session = initiateFlow(party); - Integer integer = session.receive(Integer.class).unwrap((i) -> { - return i; - }); + Integer integer = session.receive(Integer.class).unwrap((i) -> i); return integer.toString(); @@ -120,6 +134,24 @@ public class InteractiveShellJavaTest { } } + @ToStringSerialize + public static class UserValue { + private final String label; + + public UserValue(@JsonProperty("label") String label) { + this.label = label; + } + + public String getLabel() { + return label; + } + + @Override + public String toString() { + return label; + } + } + private InMemoryIdentityService ids = new InMemoryIdentityService(Lists.newArrayList(megaCorp.getIdentity()), InternalTestConstantsKt.getDEV_ROOT_CA().getCertificate()); private ObjectMapper om = JacksonSupport.createInMemoryMapper(ids, new YAMLFactory()); @@ -158,9 +190,35 @@ public class InteractiveShellJavaTest { @Test public void flowStartWithNestedTypes() throws InteractiveShell.NoApplicableConstructor { check( - "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", - "($100.12, df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587)", - FlowA.class); + "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", + "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)", + FlowA.class); + } + + @Test + public void flowStartWithUserAmount() throws InteractiveShell.NoApplicableConstructor { + check( + "b: 500, amount: { \"quantity\": 10001, \"token\":{ \"label\": \"of value\" } }", + "10501 of value", + FlowA.class); + } + + @Test + public void flowStartWithArrayType() throws InteractiveShell.NoApplicableConstructor { + check( + "b: [ One, Two, Three, Four ]", + "One+Two+Three+Four", + FlowA.class + ); + } + + @Test + public void flowStartWithArrayOfNestedType() throws InteractiveShell.NoApplicableConstructor { + check( + "amounts: [ { \"quantity\": 10, \"token\": { \"label\": \"(1)\" } }, { \"quantity\": 200, \"token\": { \"label\": \"(2)\" } } ]", + "10 (1)++200 (2)", + FlowA.class + ); } @Test(expected = InteractiveShell.NoApplicableConstructor.class) diff --git a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt index eef680f99e..6d7f614a0f 100644 --- a/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt +++ b/tools/shell/src/test/kotlin/net/corda/tools/shell/InteractiveShellTest.kt @@ -1,7 +1,9 @@ package net.corda.tools.shell +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.dataformat.yaml.YAMLFactory import net.corda.client.jackson.JacksonSupport +import net.corda.client.jackson.internal.ToStringSerialize import net.corda.core.contracts.Amount import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic @@ -31,6 +33,9 @@ class InteractiveShellTest { constructor(amount: Amount) : this(amount.toString()) constructor(pair: Pair, SecureHash.SHA256>) : this(pair.toString()) constructor(party: Party) : this(party.name.toString()) + constructor(b: Int?, amount: Amount) : this("${(b ?: 0) + amount.quantity} ${amount.token}") + constructor(b: Array) : this(b.joinToString("+")) + constructor(amounts: Array>) : this(amounts.map(Amount::toString).joinToString("++")) override val progressTracker = ProgressTracker() override fun call() = a @@ -65,8 +70,26 @@ class InteractiveShellTest { @Test fun flowStartWithNestedTypes() = check( - "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", - "($100.12, df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587)" + input = "pair: { first: $100.12, second: df489807f81c8c8829e509e1bcb92e6692b9dd9d624b7456435cb2f51dc82587 }", + expected = "(100.12 USD, DF489807F81C8C8829E509E1BCB92E6692B9DD9D624B7456435CB2F51DC82587)" + ) + + @Test + fun flowStartWithArrayType() = check( + input = "b: [ One, Two, Three, Four ]", + expected = "One+Two+Three+Four" + ) + + @Test + fun flowStartWithUserAmount() = check( + input = """b: 500, amount: { "quantity": 10001, "token":{ "label": "of value" } }""", + expected = "10501 of value" + ) + + @Test + fun flowStartWithArrayOfNestedTypes() = check( + input = """amounts: [ { "quantity": 10, "token": { "label": "(1)" } }, { "quantity": 200, "token": { "label": "(2)" } } ]""", + expected = "10 (1)++200 (2)" ) @Test(expected = InteractiveShell.NoApplicableConstructor::class) @@ -80,4 +103,9 @@ class InteractiveShellTest { @Test fun party() = check("party: \"${megaCorp.name}\"", megaCorp.name.toString()) + + @ToStringSerialize + data class UserValue(@JsonProperty("label") val label: String) { + override fun toString() = label + } }