From d3c54798265edcd80d75f5e098731aa266903be3 Mon Sep 17 00:00:00 2001 From: Shams Asari Date: Mon, 22 Oct 2018 18:56:30 +0100 Subject: [PATCH 1/9] CORDA-1621: The finance CorDapp uses the app config feature rather than the node's config (#4100) --- docs/source/testnet-explorer-corda.rst | 45 ++++++----- .../node/configuration/Configuration.kt | 5 +- .../configuration/CordappConfiguration.kt | 2 + .../configuration/CurrencyConfiguration.kt | 4 + .../corda/behave/scenarios/helpers/Cash.kt | 2 +- .../flows/test/CashConfigDataFlowTest.kt | 24 ------ .../corda/finance/flows/CashConfigDataFlow.kt | 77 ------------------- .../finance/internal/CashConfigDataFlow.kt | 48 ++++++++++++ .../internal/CashConfigDataFlowTest.kt | 29 +++++++ ...owCheckpointVersionNodeStartupCheckTest.kt | 9 +-- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../cordapp/CordappConfigFileProvider.kt | 24 +++--- .../cordapp/CordappConfigFileProviderTests.kt | 6 +- samples/bank-of-corda-demo/build.gradle | 4 +- .../net/corda/testing/node/TestCordapp.kt | 11 ++- .../node/internal/TestCordappDirectories.kt | 24 +++--- .../testing/node/internal/TestCordappImpl.kt | 3 + .../net/corda/demobench/explorer/Explorer.kt | 2 +- .../net/corda/demobench/model/NodeConfig.kt | 31 ++++---- .../corda/demobench/model/NodeController.kt | 12 +-- .../demobench/plugin/CordappController.kt | 13 ++-- .../demobench/profile/ProfileController.kt | 2 +- .../corda/demobench/model/NodeConfigTest.kt | 7 +- .../net/corda/explorer/ExplorerSimulation.kt | 28 +++++-- .../net/corda/explorer/model/IssuerModel.kt | 2 +- 25 files changed, 213 insertions(+), 203 deletions(-) delete mode 100644 finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt delete mode 100644 finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt create mode 100644 finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt create mode 100644 finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt diff --git a/docs/source/testnet-explorer-corda.rst b/docs/source/testnet-explorer-corda.rst index c9217dfca2..e8902a3b3c 100644 --- a/docs/source/testnet-explorer-corda.rst +++ b/docs/source/testnet-explorer-corda.rst @@ -20,17 +20,15 @@ Get the testing tools To run the tests and make sure your node is connecting correctly to the network you will need to download and install a couple of resources. -1. Log into your Cloud VM via SSH. +#. Log into your Cloud VM via SSH. - -2. Stop the Corda node(s) running on your cloud instance. +#. Stop the Corda node(s) running on your cloud instance. .. code:: bash ps aux | grep corda.jar | awk '{ print $2 }' | xargs sudo kill - -3. Download the finance CorDapp +#. Download the finance CorDapp In the terminal on your cloud instance run: @@ -38,32 +36,32 @@ couple of resources. wget https://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-finance/-corda/corda-finance--corda.jar - This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to the Corda installation location: + This is required to run some flows to check your connections, and to issue/transfer cash to counterparties. Copy it to + the Corda installation location: .. code:: bash sudo cp /home//corda-finance--corda.jar /opt/corda/cordapps/ -4. Add the following line to the bottom of your ``node.conf``: +#. Run the following to create a config file for the finance CorDapp: .. code:: bash - issuableCurrencies : [ USD ] + echo "issuableCurrencies : [ USD ]" > /opt/corda/cordapps/config/corda-finance--corda.conf - .. note:: Make sure that the config file is in the correct format, e.g., by ensuring that there's a comma at the end of the line prior to the added config. - -4. Restart the Corda node: +#. Restart the Corda node: .. code:: bash cd /opt/corda sudo ./run-corda.sh - Your node is now running the Finance Cordapp. + Your node is now running the finance Cordapp. - .. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-.log``. This file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs. + .. note:: You can double-check that the CorDapp is loaded in the log file ``/opt/corda/logs/node-.log``. This + file will list installed apps at startup. Search for ``Loaded CorDapps`` in the logs. -6. Now download the Node Explorer to your **LOCAL** machine: +#. Now download the Node Explorer to your **LOCAL** machine: .. note:: Node Explorer is a JavaFX GUI which connects to the node over the RPC interface and allows you to send transactions. @@ -73,9 +71,10 @@ couple of resources. http://ci-artifactory.corda.r3cev.com/artifactory/corda-releases/net/corda/corda-tools-explorer/-corda/corda-tools-explorer--corda.jar - .. warning:: This Node Explorer is incompatible with the Corda Enterprise distribution and vice versa as they currently use different serialisation schemes (Kryo vs AMQP). + .. warning:: This Node Explorer is incompatible with the Corda Enterprise distribution and vice versa as they currently + use different serialisation schemes (Kryo vs AMQP). -7. Run the Node Explorer tool on your **LOCAL** machine. +#. Run the Node Explorer tool on your **LOCAL** machine. .. code:: bash @@ -90,8 +89,10 @@ Connect to the node To connect to the node you will need: * The IP address of your node (the public IP of your cloud instance). You can find this in the instance page of your cloud console. -* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section, (by default this is 10003 on Testnet). -* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section, (by default the username is ``cordazoneservice`` on Testnet). +* The port number of the RPC interface to the node, specified in ``/opt/corda/node.conf`` in the ``rpcSettings`` section, + (by default this is 10003 on Testnet). +* The username and password of the RPC interface of the node, also in the ``node.conf`` in the ``rpcUsers`` section, + (by default the username is ``cordazoneservice`` on Testnet). Click on ``Connect`` to log into the node. @@ -102,7 +103,8 @@ Once Explorer has logged in to your node over RPC click on the ``Network`` tab i .. image:: resources/explorer-network.png -If your Corda node is correctly configured and connected to the Testnet then you should be able to see the identities of your node, the Testnet notary and the network map listing all the counterparties currently on the network. +If your Corda node is correctly configured and connected to the Testnet then you should be able to see the identities of +your node, the Testnet notary and the network map listing all the counterparties currently on the network. Test issuance transaction @@ -120,8 +122,9 @@ Click ``Execute`` and the transaction will start. .. image:: resources/explorer-cash-issue3.png -Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress, or wait for a success message to be displayed: +Click on the red X to close the notification window and click on ``Transactions`` tab to see the transaction in progress, +or wait for a success message to be displayed: .. image:: resources/explorer-transactions.png -Congratulations! You have now successfully installed a CorDapp and executed a transaction on the Corda Testnet. \ No newline at end of file +Congratulations! You have now successfully installed a CorDapp and executed a transaction on the Corda Testnet. diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt index 6ec510c75b..de6450a6ca 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/Configuration.kt @@ -21,6 +21,7 @@ class Configuration( nodeInterface.dbPort, password = DEFAULT_PASSWORD ), + // TODO This is not being used when it could be. The call-site is using configElements instead. val notary: NotaryConfiguration = NotaryConfiguration(), val cordapps: CordappConfiguration = CordappConfiguration(), vararg configElements: ConfigurationTemplate @@ -28,9 +29,7 @@ class Configuration( private val developerMode = true - val cordaX500Name: CordaX500Name by lazy({ - CordaX500Name(name, location, country) - }) + val cordaX500Name: CordaX500Name = CordaX500Name(name, location, country) private val basicConfig = """ |myLegalName="C=$country,L=$location,O=$name" diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt index 57a1f96e6b..b288cb9514 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CordappConfiguration.kt @@ -1,5 +1,7 @@ package net.corda.behave.node.configuration +// TODO This is a ConfigurationTemplate but is never used as one. Therefore the private "applications" list is never used +// and thus includeFinance isn't necessary either. Something is amiss. class CordappConfiguration(var apps: List = emptyList(), val includeFinance: Boolean = false) : ConfigurationTemplate() { private val applications = apps + if (includeFinance) { diff --git a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt index 16252650d2..301aad7d24 100644 --- a/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt +++ b/experimental/behave/src/main/kotlin/net/corda/behave/node/configuration/CurrencyConfiguration.kt @@ -7,6 +7,10 @@ class CurrencyConfiguration(private val issuableCurrencies: List) : Conf if (issuableCurrencies.isEmpty()) { "" } else { + // TODO This is no longer correct. issuableCurrencies is a config of the finance app and belongs + // in a separate .conf file for the app (in the config sub-directory, with a filename matching the CorDapp + // jar filename). It is no longer read in from the node conf file. There seem to be pieces missing in the + // behave framework to allow one to do this. """ |custom : { | issuableCurrencies : [ diff --git a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt index 6930331f10..fbaec51f64 100644 --- a/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt +++ b/experimental/behave/src/scenario/kotlin/net/corda/behave/scenarios/helpers/Cash.kt @@ -7,9 +7,9 @@ import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.OpaqueBytes import net.corda.core.utilities.getOrThrow -import net.corda.finance.flows.CashConfigDataFlow import net.corda.finance.flows.CashIssueFlow import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.internal.CashConfigDataFlow import java.util.* import java.util.concurrent.TimeUnit diff --git a/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt b/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt deleted file mode 100644 index 77316c148e..0000000000 --- a/finance/src/integration-test/kotlin/net/corda/finance/flows/test/CashConfigDataFlowTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.corda.finance.flows.test - -import net.corda.core.messaging.startFlow -import net.corda.core.utilities.getOrThrow -import net.corda.finance.EUR -import net.corda.finance.USD -import net.corda.finance.flows.CashConfigDataFlow -import net.corda.testing.driver.DriverParameters -import net.corda.testing.driver.driver -import org.assertj.core.api.Assertions.assertThat -import org.junit.Test - -class CashConfigDataFlowTest { - @Test - fun `issuable currencies are read in from node config`() { - driver(DriverParameters( - extraCordappPackagesToScan = listOf("net.corda.finance.flows"), - notarySpecs = emptyList())) { - val node = startNode(customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("EUR", "USD")))).getOrThrow() - val config = node.rpc.startFlow(::CashConfigDataFlow).returnValue.getOrThrow() - assertThat(config.issuableCurrencies).containsExactly(EUR, USD) - } - } -} diff --git a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt deleted file mode 100644 index ac890bd517..0000000000 --- a/finance/src/main/kotlin/net/corda/finance/flows/CashConfigDataFlow.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.corda.finance.flows - -import co.paralleluniverse.fibers.Suspendable -import com.typesafe.config.ConfigFactory -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.StartableByRPC -import net.corda.core.internal.declaredField -import net.corda.core.node.AppServiceHub -import net.corda.core.node.services.CordaService -import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.finance.CHF -import net.corda.finance.EUR -import net.corda.finance.GBP -import net.corda.finance.USD -import net.corda.finance.flows.ConfigHolder.Companion.supportedCurrencies -import java.io.IOException -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.OpenOption -import java.nio.file.Path -import java.nio.file.Paths -import java.util.* - -// TODO Until apps have access to their own config, we'll hack things by first getting the baseDirectory, read the node.conf -// again to get our config and store it here for access by our flow -@CordaService -class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { - companion object { - val supportedCurrencies = listOf(USD, GBP, CHF, EUR) - - // TODO: In future releases, the Finance app should be fully decoupled from internal APIs in Core. - private operator fun Path.div(other: String): Path = resolve(other) - private operator fun String.div(other: String): Path = Paths.get(this) / other - private fun Path.inputStream(vararg options: OpenOption): InputStream = Files.newInputStream(this, *options) - private inline fun Path.read(vararg options: OpenOption, block: (InputStream) -> R): R = inputStream(*options).use(block) - } - - val issuableCurrencies: List - - init { - // Warning!! You are about to see a major hack! - val baseDirectory = services.declaredField("serviceHub").value - .let { it.javaClass.getMethod("getConfiguration").apply { isAccessible = true }.invoke(it) } - .let { it.javaClass.getMethod("getBaseDirectory").apply { isAccessible = true }.invoke(it) } - .let { it.javaClass.getMethod("toString").apply { isAccessible = true }.invoke(it) as String } - - var issuableCurrenciesValue: List - try { - val config = (baseDirectory / "node.conf").read { ConfigFactory.parseReader(it.reader()) } - if (config.hasPath("custom.issuableCurrencies")) { - issuableCurrenciesValue = config.getStringList("custom.issuableCurrencies").map { Currency.getInstance(it) } - require(supportedCurrencies.containsAll(issuableCurrenciesValue)) - } else { - issuableCurrenciesValue = emptyList() - } - } catch (e: IOException) { - issuableCurrenciesValue = emptyList() - } - issuableCurrencies = issuableCurrenciesValue - } -} - -/** - * Flow to obtain cash cordapp app configuration. - */ -@StartableByRPC -class CashConfigDataFlow : FlowLogic() { - @Suspendable - override fun call(): CashConfiguration { - val configHolder = serviceHub.cordaService(ConfigHolder::class.java) - return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) - } -} - -@CordaSerializable -data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt b/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt new file mode 100644 index 0000000000..3c2ab3c988 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/finance/internal/CashConfigDataFlow.kt @@ -0,0 +1,48 @@ +package net.corda.finance.internal + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.internal.uncheckedCast +import net.corda.core.node.AppServiceHub +import net.corda.core.node.services.CordaService +import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.SingletonSerializeAsToken +import net.corda.finance.CHF +import net.corda.finance.EUR +import net.corda.finance.GBP +import net.corda.finance.USD +import net.corda.finance.internal.ConfigHolder.Companion.supportedCurrencies +import java.util.* + +@CordaService +class ConfigHolder(services: AppServiceHub) : SingletonSerializeAsToken() { + companion object { + val supportedCurrencies = listOf(USD, GBP, CHF, EUR) + } + + val issuableCurrencies: List + + init { + val issuableCurrenciesStringList: List = uncheckedCast(services.getAppContext().config.get("issuableCurrencies")) + issuableCurrencies = issuableCurrenciesStringList.map(Currency::getInstance) + (issuableCurrencies - supportedCurrencies).let { + require(it.isEmpty()) { "$it are not supported currencies" } + } + } +} + +/** + * Flow to obtain cash cordapp app configuration. + */ +@StartableByRPC +class CashConfigDataFlow : FlowLogic() { + @Suspendable + override fun call(): CashConfiguration { + val configHolder = serviceHub.cordaService(ConfigHolder::class.java) + return CashConfiguration(configHolder.issuableCurrencies, supportedCurrencies) + } +} + +@CordaSerializable +data class CashConfiguration(val issuableCurrencies: List, val supportedCurrencies: List) diff --git a/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt b/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt new file mode 100644 index 0000000000..669f1a2c9a --- /dev/null +++ b/finance/src/test/kotlin/net/corda/finance/internal/CashConfigDataFlowTest.kt @@ -0,0 +1,29 @@ +package net.corda.finance.internal + +import net.corda.core.internal.packageName +import net.corda.core.utilities.getOrThrow +import net.corda.finance.EUR +import net.corda.finance.USD +import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockNetworkParameters +import net.corda.testing.node.MockNodeParameters +import net.corda.testing.node.internal.cordappForPackages +import org.assertj.core.api.Assertions.assertThat +import org.junit.After +import org.junit.Test + +class CashConfigDataFlowTest { + private val mockNet = MockNetwork(emptyList(), MockNetworkParameters(threadPerNode = true)) + + @After + fun cleanUp() = mockNet.stopNodes() + + @Test + fun `issuable currencies read in from cordapp config`() { + val node = mockNet.createNode(MockNodeParameters(additionalCordapps = listOf( + cordappForPackages(javaClass.packageName).withConfig(mapOf("issuableCurrencies" to listOf("EUR", "USD"))) + ))) + val config = node.startFlow(CashConfigDataFlow()).getOrThrow() + assertThat(config.issuableCurrencies).containsExactly(EUR, USD) + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt index 169ba207ce..40e84e57fb 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowCheckpointVersionNodeStartupCheckTest.kt @@ -69,12 +69,11 @@ class FlowCheckpointVersionNodeStartupCheckTest { // Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can // rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed. val cordappDir = TestCordappDirectories.getJarDirectory(cordapp) - val cordappJar = cordappDir.list().single() + val cordappJar = cordappDir.list().single { it.toString().endsWith(".jar") } createSuspendedFlowInBob(setOf(cordapp)) - // Rename the jar file. TestCordappDirectories caches the location of the jar file but the use of the random - // UUID in the name means there's zero chance of contaminating another test. + // Rename the jar file. cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}") assertBobFailsToStartWithLogMessage( @@ -88,13 +87,13 @@ class FlowCheckpointVersionNodeStartupCheckTest { fun `restart node with incompatible version of suspended flow due to different jar hash`() { driver(parametersForRestartingNodes()) { val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}") - val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single() + val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single { it.toString().endsWith(".jar") } createSuspendedFlowInBob(setOf(originalCordapp)) // The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified") - val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single() + val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single { it.toString().endsWith(".jar") } modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING) assertBobFailsToStartWithLogMessage( diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 4bee70d8c6..d8b9aa15a6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -167,7 +167,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize() val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) } val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize() - val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize() + val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize() @Suppress("LeakingThis") val keyManagementService = makeKeyManagementService(identityService).tokenize() val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also { diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt index dbf4daf657..5a6e8377b6 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProvider.kt @@ -5,30 +5,26 @@ import com.typesafe.config.ConfigFactory import net.corda.core.internal.createDirectories import net.corda.core.internal.div import net.corda.core.internal.exists -import net.corda.core.internal.isDirectory +import net.corda.core.internal.noneOrSingle import net.corda.core.utilities.contextLogger import java.nio.file.Path -import java.nio.file.Paths -class CordappConfigFileProvider(private val configDir: Path = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider { +class CordappConfigFileProvider(cordappDirectories: List) : CordappConfigProvider { companion object { - val DEFAULT_CORDAPP_CONFIG_DIR = Paths.get("cordapps") / "config" - const val CONFIG_EXT = ".conf" - val logger = contextLogger() + private val logger = contextLogger() } - init { - configDir.createDirectories() - } + private val configDirectories = cordappDirectories.map { (it / "config").createDirectories() } override fun getConfigByName(name: String): Config { - val configFile = configDir / "$name$CONFIG_EXT" - return if (configFile.exists()) { - check(!configFile.isDirectory()) { "${configFile.toAbsolutePath()} is a directory, expected a config file" } - logger.info("Found config for cordapp $name in ${configFile.toAbsolutePath()}") + // TODO There's nothing stopping the same CorDapp jar from occuring in different directories and thus causing + // conflicts. The cordappDirectories list config option should just be a single cordappDirectory + val configFile = configDirectories.map { it / "$name.conf" }.noneOrSingle { it.exists() } + return if (configFile != null) { + logger.info("Found config for cordapp $name in $configFile") ConfigFactory.parseFile(configFile.toFile()) } else { - logger.info("No config found for cordapp $name in ${configFile.toAbsolutePath()}") + logger.info("No config found for cordapp $name in $configDirectories") ConfigFactory.empty() } } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt index f3ea03a72e..73829d5a8c 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/CordappConfigFileProviderTests.kt @@ -12,16 +12,16 @@ import java.nio.file.Paths class CordappConfigFileProviderTests { private companion object { - val cordappConfDir = Paths.get("build") / "tmp" / "cordapps" / "config" + val cordappDir = Paths.get("build") / "tmp" / "cordapps" const val cordappName = "test" - val cordappConfFile = cordappConfDir / "$cordappName.conf" + val cordappConfFile = cordappDir / "config" / "$cordappName.conf" val validConfig: Config = ConfigFactory.parseString("key=value") val alternateValidConfig: Config = ConfigFactory.parseString("key=alternateValue") const val invalidConfig = "Invalid" } - private val provider = CordappConfigFileProvider(cordappConfDir) + private val provider = CordappConfigFileProvider(listOf(cordappDir)) @Test fun `test that config can be loaded`() { diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 0990cc5987..135a51c921 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -56,9 +56,11 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, webPort 10007 rpcUsers = [[user: "bankUser", password: "test", permissions: ["ALL"]]] extraConfig = [ - custom : [issuableCurrencies: ["USD"]], h2Settings: [address: "localhost:10017"] ] + cordapp(project(':finance')) { + config "issuableCurrencies = [ USD ]" + } } node { name "O=BigCorporation,L=New York,C=US" diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt index 6f881660c1..fed1f14e22 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/TestCordapp.kt @@ -19,12 +19,15 @@ interface TestCordapp { /** Returns the version string, defaults to "1.0" if not specified. */ val version: String - /** Returns the vendor string, defaults to "Corda" if not specified. */ + /** Returns the vendor string, defaults to "test-vendor" if not specified. */ val vendor: String /** Returns the target platform version, defaults to the current platform version if not specified. */ val targetVersion: Int + /** Returns the config for this CorDapp, defaults to empty if not specified. */ + val config: Map + /** Returns the set of package names scanned for this test CorDapp. */ val packages: Set @@ -43,6 +46,9 @@ interface TestCordapp { /** Return a copy of this [TestCordapp] but with the specified target platform version. */ fun withTargetVersion(targetVersion: Int): TestCordapp + /** Returns a copy of this [TestCordapp] but with the specified CorDapp config. */ + fun withConfig(config: Map): TestCordapp + class Factory { companion object { @JvmStatic @@ -57,9 +63,10 @@ interface TestCordapp { return TestCordappImpl( name = "test-cordapp", version = "1.0", - vendor = "Corda", + vendor = "test-vendor", title = "test-title", targetVersion = PLATFORM_VERSION, + config = emptyMap(), packages = simplifyScanPackages(packageNames), classes = emptySet() ) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt index 3cd553de39..354aebe183 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappDirectories.kt @@ -1,9 +1,11 @@ package net.corda.testing.node.internal +import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.sha256 import net.corda.core.internal.createDirectories import net.corda.core.internal.deleteRecursively import net.corda.core.internal.div +import net.corda.core.internal.writeText import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.testing.node.TestCordapp @@ -22,24 +24,28 @@ object TestCordappDirectories { fun getJarDirectory(cordapp: TestCordapp, cordappsDirectory: Path = defaultCordappsDirectory): Path { cordapp as TestCordappImpl return testCordappsCache.computeIfAbsent(cordapp) { - val cordappDir = (cordappsDirectory / UUID.randomUUID().toString()).createDirectories() - val uniqueScanString = if (cordapp.packages.size == 1 && cordapp.classes.isEmpty()) { - cordapp.packages.first() - } else { - "${cordapp.packages}${cordapp.classes.joinToString { it.name }}".toByteArray().sha256().toString() + val configString = ConfigValueFactory.fromMap(cordapp.config).toConfig().root().render() + val filename = cordapp.run { + val uniqueScanString = if (packages.size == 1 && classes.isEmpty() && config.isEmpty()) { + packages.first() + } else { + "$packages$classes$configString".toByteArray().sha256().toString() + } + "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString".replace(whitespace, "-") } - val jarFileName = cordapp.run { "${name}_${vendor}_${title}_${version}_${targetVersion}_$uniqueScanString.jar".replace(whitespace, "-") } - val jarFile = cordappDir / jarFileName + val cordappDir = cordappsDirectory / UUID.randomUUID().toString() + val configDir = (cordappDir / "config").createDirectories() + val jarFile = cordappDir / "$filename.jar" cordapp.packageAsJar(jarFile) + (configDir / "$filename.conf").writeText(configString) logger.debug { "$cordapp packaged into $jarFile" } cordappDir } } private val defaultCordappsDirectory: Path by lazy { - val cordappsDirectory = (Paths.get("build") / "tmp" / getTimestampAsDirectoryName() / "generated-test-cordapps").toAbsolutePath() + val cordappsDirectory = Paths.get("build").toAbsolutePath() / "generated-test-cordapps" / getTimestampAsDirectoryName() logger.info("Initialising generated test CorDapps directory in $cordappsDirectory") - cordappsDirectory.toFile().deleteOnExit() cordappsDirectory.deleteRecursively() cordappsDirectory.createDirectories() } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt index 831198320c..c8e7ab626b 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/TestCordappImpl.kt @@ -7,6 +7,7 @@ data class TestCordappImpl(override val name: String, override val vendor: String, override val title: String, override val targetVersion: Int, + override val config: Map, override val packages: Set, val classes: Set>) : TestCordapp { @@ -20,6 +21,8 @@ data class TestCordappImpl(override val name: String, override fun withTargetVersion(targetVersion: Int): TestCordappImpl = copy(targetVersion = targetVersion) + override fun withConfig(config: Map): TestCordappImpl = copy(config = config) + fun withClasses(vararg classes: Class<*>): TestCordappImpl { return copy(classes = classes.filter { clazz -> packages.none { clazz.name.startsWith("$it.") } }.toSet()) } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 4606621280..7baabdb8fe 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -85,7 +85,7 @@ class Explorer internal constructor(private val explorerController: ExplorerCont // Note: does not copy dependencies because we should soon be making all apps fat jars and dependencies implicit. // // TODO: Remove this code when serialisation has been upgraded. - val cordappsDir = config.explorerDir / NodeConfig.cordappDirName + val cordappsDir = config.explorerDir / NodeConfig.CORDAPP_DIR_NAME cordappsDir.createDirectories() config.cordappsDir.list { it.forEachOrdered { path -> diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 9f63d8f38b..5ae40f6eb4 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -3,6 +3,7 @@ package net.corda.demobench.model import com.typesafe.config.* import com.typesafe.config.ConfigFactory.empty import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.VisibleForTesting import net.corda.core.internal.copyToDirectory import net.corda.core.internal.createDirectories import net.corda.core.internal.div @@ -11,7 +12,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.config.toConfig import java.nio.file.Path import java.nio.file.StandardCopyOption -import java.util.Properties +import java.util.* /** * This is a subset of FullNodeConfiguration, containing only those configs which we need. The node uses reference.conf @@ -38,33 +39,33 @@ data class NodeConfig( companion object { val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) val defaultUser = user("guest") - const val cordappDirName = "cordapps" + const val CORDAPP_DIR_NAME = "cordapps" } - fun nodeConf(): Config { + @VisibleForTesting + internal fun nodeConf(): Config { val rpcSettings: ConfigObject = empty() .withValue("address", valueFor(rpcSettings.address.toString())) .withValue("adminAddress", valueFor(rpcSettings.adminAddress.toString())) .root() - val customMap: Map = HashMap().also { - if (issuableCurrencies.isNotEmpty()) { - it["issuableCurrencies"] = issuableCurrencies - } - } - val custom: ConfigObject = ConfigFactory.parseMap(customMap).root() return NodeConfigurationData(myLegalName, p2pAddress, this.rpcSettings.address, notary, h2port, rpcUsers, useTestClock, detectPublicIp, devMode) .toConfig() .withoutPath("rpcAddress") .withoutPath("rpcAdminAddress") .withValue("rpcSettings", rpcSettings) - .withOptionalValue("custom", custom) } - fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig() + @VisibleForTesting + internal fun webServerConf() = WebServerConfigurationData(myLegalName, rpcSettings.address, webAddress, rpcUsers).asConfig() - fun toNodeConfText() = nodeConf().render() + fun toNodeConfText(): String = nodeConf().render() - fun toWebServerConfText() = webServerConf().render() + fun toWebServerConfText(): String = webServerConf().render() + + @VisibleForTesting + internal fun financeConf() = FinanceConfData(issuableCurrencies).toConfig() + + fun toFinanceConfText(): String = financeConf().render() fun serialiseAsString(): String = toConfig().render() @@ -92,6 +93,8 @@ private data class WebServerConfigurationData( fun asConfig() = toConfig() } +private data class FinanceConfData(val issuableCurrencies: List) + /** * This is a subset of NotaryConfig. It implements [ExtraService] to avoid unnecessary copying. */ @@ -104,7 +107,7 @@ data class NodeConfigWrapper(val baseDir: Path, val nodeConfig: NodeConfig) : Ha val key: String = nodeConfig.myLegalName.organisation.toKey() val nodeDir: Path = baseDir / key val explorerDir: Path = baseDir / "$key-explorer" - override val cordappsDir: Path = nodeDir / NodeConfig.cordappDirName + override val cordappsDir: Path = nodeDir / NodeConfig.CORDAPP_DIR_NAME var state: NodeState = NodeState.STARTING fun install(cordapps: Collection) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index 05e1bdc3e3..09d12ebf03 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -112,18 +112,14 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { try { // Notary can be removed and then added again, that's why we need to perform this check. require((config.nodeConfig.notary != null).xor(notaryIdentity != null)) { "There must be exactly one notary in the network" } - config.nodeDir.createDirectories() + val cordappConfigDir = (config.cordappsDir / "config").createDirectories() // Install any built-in plugins into the working directory. cordappController.populate(config) - // Write this node's configuration file into its working directory. - val confFile = config.nodeDir / "node.conf" - confFile.writeText(config.nodeConfig.toNodeConfText()) - - // Write this node's configuration file into its working directory. - val webConfFile = config.nodeDir / "web-server.conf" - webConfFile.writeText(config.nodeConfig.toWebServerConfText()) + (config.nodeDir / "node.conf").writeText(config.nodeConfig.toNodeConfText()) + (config.nodeDir / "web-server.conf").writeText(config.nodeConfig.toWebServerConfText()) + (cordappConfigDir / "${CordappController.FINANCE_CORDAPP_FILENAME}.conf").writeText(config.nodeConfig.toFinanceConfText()) // Execute the Corda node val cordaEnv = System.getenv().toMutableMap().apply { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt index 66f844e885..b82d9c7779 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/plugin/CordappController.kt @@ -12,10 +12,13 @@ import java.nio.file.StandardCopyOption import kotlin.streams.toList class CordappController : Controller() { + companion object { + const val FINANCE_CORDAPP_FILENAME = "corda-finance" + } private val jvm by inject() - private val cordappDir: Path = jvm.applicationDir.resolve(NodeConfig.cordappDirName) - private val finance: Path = cordappDir.resolve("corda-finance.jar") + private val cordappDir: Path = jvm.applicationDir / NodeConfig.CORDAPP_DIR_NAME + private val financeCordappJar: Path = cordappDir / "$FINANCE_CORDAPP_FILENAME.jar" /** * Install any built-in cordapps that this node requires. @@ -25,8 +28,8 @@ class CordappController : Controller() { if (!config.cordappsDir.exists()) { config.cordappsDir.createDirectories() } - if (finance.exists()) { - finance.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) + if (financeCordappJar.exists()) { + financeCordappJar.copyToDirectory(config.cordappsDir, StandardCopyOption.REPLACE_EXISTING) log.info("Installed 'Finance' cordapp") } } @@ -39,7 +42,7 @@ class CordappController : Controller() { if (!config.cordappsDir.isDirectory()) return emptyList() return config.cordappsDir.walk(1) { paths -> paths.filter(Path::isCordapp) - .filter { !finance.endsWith(it.fileName) } + .filter { !financeCordappJar.endsWith(it.fileName) } .toList() } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt index 7c034983b8..4c8bfeb02d 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/profile/ProfileController.kt @@ -60,7 +60,7 @@ class ProfileController : Controller() { log.info("Wrote: $file") // Write all of the non-built-in cordapps. - val cordappDir = (nodeDir / NodeConfig.cordappDirName).createDirectory() + val cordappDir = (nodeDir / NodeConfig.CORDAPP_DIR_NAME).createDirectory() cordappController.useCordappsFor(config).forEach { val cordapp = it.copyToDirectory(cordappDir) log.info("Wrote: $cordapp") diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index d044c63f14..e07f18331b 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -65,12 +65,7 @@ class NodeConfigTest { issuableCurrencies = listOf("GBP") ) - val nodeConfig = config.nodeConf() - .withValue("baseDirectory", valueFor(baseDir.toString())) - .withFallback(ConfigFactory.parseResources("reference.conf")) - .resolve() - val custom = nodeConfig.getConfig("custom") - assertEquals(listOf("GBP"), custom.getAnyRefList("issuableCurrencies")) + assertEquals(listOf("GBP"), config.financeConf().getStringList("issuableCurrencies")) } @Test diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index b44c2b7af4..fd4c43fcf9 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -18,14 +18,19 @@ import net.corda.core.utilities.getOrThrow import net.corda.finance.GBP import net.corda.finance.USD import net.corda.finance.contracts.asset.Cash -import net.corda.finance.flows.* +import net.corda.finance.flows.AbstractCashFlow +import net.corda.finance.flows.CashExitFlow import net.corda.finance.flows.CashExitFlow.ExitRequest +import net.corda.finance.flows.CashIssueAndPaymentFlow import net.corda.finance.flows.CashIssueAndPaymentFlow.IssueAndPaymentRequest +import net.corda.finance.flows.CashPaymentFlow +import net.corda.finance.internal.CashConfigDataFlow import net.corda.node.services.Permissions.Companion.startFlow import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.driver.* import net.corda.testing.node.User +import net.corda.testing.node.internal.FINANCE_CORDAPP import java.time.Instant import java.util.* @@ -63,16 +68,27 @@ class ExplorerSimulation(private val options: OptionSet) { private fun startDemoNodes() { val portAllocation = PortAllocation.Incremental(20000) - driver(DriverParameters(portAllocation = portAllocation, extraCordappPackagesToScan = listOf("net.corda.finance"), waitForAllNodesToFinish = true, jmxPolicy = JmxPolicy(true))) { + driver(DriverParameters( + portAllocation = portAllocation, + cordappsForAllNodes = listOf(FINANCE_CORDAPP), + waitForAllNodesToFinish = true, + jmxPolicy = JmxPolicy(true) + )) { // TODO : Supported flow should be exposed somehow from the node instead of set of ServiceInfo. val alice = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)) val bob = startNode(providedName = BOB_NAME, rpcUsers = listOf(user)) val ukBankName = CordaX500Name(organisation = "UK Bank Plc", locality = "London", country = "GB") val usaBankName = CordaX500Name(organisation = "USA Bank Corp", locality = "New York", country = "US") - val issuerGBP = startNode(providedName = ukBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("GBP")))) - val issuerUSD = startNode(providedName = usaBankName, rpcUsers = listOf(manager), - customOverrides = mapOf("custom" to mapOf("issuableCurrencies" to listOf("USD")))) + val issuerGBP = startNode( + providedName = ukBankName, + rpcUsers = listOf(manager), + additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("GBP")))) + ) + val issuerUSD = startNode( + providedName = usaBankName, + rpcUsers = listOf(manager), + additionalCordapps = listOf(FINANCE_CORDAPP.withConfig(mapOf("issuableCurrencies" to listOf("USD")))) + ) notaryNode = defaultNotaryNode.get() aliceNode = alice.get() diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt index 56049a35ee..3226466630 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt @@ -7,7 +7,7 @@ import net.corda.client.jfx.utils.ChosenList import net.corda.client.jfx.utils.map import net.corda.core.messaging.startFlow import net.corda.core.utilities.getOrThrow -import net.corda.finance.flows.CashConfigDataFlow +import net.corda.finance.internal.CashConfigDataFlow import tornadofx.* import java.util.* From 268b544b4b94fec0f9a9f2231afeedf0ded46a40 Mon Sep 17 00:00:00 2001 From: Lamar Thomas <38670842+r3ltsupport@users.noreply.github.com> Date: Fri, 19 Oct 2018 15:23:48 -0400 Subject: [PATCH 2/9] fixed order of repudiation and information disclos --- docs/source/design/threat-model/corda-threat-model.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/design/threat-model/corda-threat-model.md b/docs/source/design/threat-model/corda-threat-model.md index 83e5a1da1f..cbec1a1769 100644 --- a/docs/source/design/threat-model/corda-threat-model.md +++ b/docs/source/design/threat-model/corda-threat-model.md @@ -46,8 +46,8 @@ threats is the [STRIDE](https://en.wikipedia.org/wiki/STRIDE_(security)) framewo - Spoofing - Tampering -- Information Disclosure - Repudiation +- Information Disclosure - Denial of Service - Elevation of Privilege From ce9f95ca86b613d462c4b77541e55ef92d83d21b Mon Sep 17 00:00:00 2001 From: Viktor Kolomeyko Date: Tue, 23 Oct 2018 11:08:30 +0100 Subject: [PATCH 3/9] ENT-2610: Correct `withBaseDirectory` method (#4106) Even though it is not used in OS, it is better to keep it in sync with Ent. --- .../corda/testing/internal/stubs/CertificateStoreStubs.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt index dfe21245f1..c93aeaeebe 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/stubs/CertificateStoreStubs.kt @@ -55,9 +55,12 @@ class CertificateStoreStubs { } @JvmStatic - fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { + fun withBaseDirectory(baseDirectory: Path, certificatesDirectoryName: String = DEFAULT_CERTIFICATES_DIRECTORY_NAME, + keyStoreFileName: String = KeyStore.DEFAULT_STORE_FILE_NAME, keyStorePassword: String = KeyStore.DEFAULT_STORE_PASSWORD, + keyPassword: String = keyStorePassword, trustStoreFileName: String = TrustStore.DEFAULT_STORE_FILE_NAME, + trustStorePassword: String = TrustStore.DEFAULT_STORE_PASSWORD): MutualSslConfiguration { - return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, trustStoreFileName, trustStorePassword) + return withCertificatesDirectory(baseDirectory / certificatesDirectoryName, keyStoreFileName, keyStorePassword, keyPassword, trustStoreFileName, trustStorePassword) } } From b9aa23d3ac44eedbe1987d4a60002a5534592362 Mon Sep 17 00:00:00 2001 From: Rick Parker Date: Tue, 23 Oct 2018 11:12:29 +0100 Subject: [PATCH 4/9] ENT-2431 Move some changes from enterprise to OS (#4101) --- .../main/kotlin/net/corda/node/utilities/NodeNamedCache.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt index 5c963ac80d..5c2b9a1241 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/NodeNamedCache.kt @@ -27,13 +27,13 @@ interface BindableNamedCacheFactory : NamedCacheFactory, SerializeAsToken { fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory } -open class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { +open class DefaultNamedCacheFactory protected constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() { constructor() : this(null, null) override fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration) override fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration) - protected fun configuredForNamed(caffeine: Caffeine, name: String): Caffeine { + open protected fun configuredForNamed(caffeine: Caffeine, name: String): Caffeine { return with(nodeConfiguration!!) { when { name.startsWith("RPCSecurityManagerShiroCache_") -> with(security?.authService?.options?.cache!!) { caffeine.maximumSize(maxEntries).expireAfterWrite(expireAfterSecs, TimeUnit.SECONDS) } @@ -77,5 +77,5 @@ open class DefaultNamedCacheFactory private constructor(private val metricRegist return configuredForNamed(caffeine, name).build(loader) } - protected val defaultCacheSize = 1024L + open protected val defaultCacheSize = 1024L } \ No newline at end of file From f8ac35df25459651d47ef768ba18f945573ad4ad Mon Sep 17 00:00:00 2001 From: Joel Dudley Date: Tue, 23 Oct 2018 16:39:48 +0200 Subject: [PATCH 5/9] Update CONTRIBUTORS.md --- CONTRIBUTORS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 31cdf7d71c..aa4befbc67 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -180,7 +180,7 @@ see changes to this list. * Scott James * Sean Zhang (Wells Fargo) * Shams Asari (R3) -* Shivan Sawant (Persistent Systems Limited) +* Shivan Sawant * Siddhartha Sengupta (Tradewind Markets) * Simon Taylor (Barclays) * Sofus Mortensen (Digital Asset Holdings) From 0919b01271b7b26c204879fe89429058358dc1de Mon Sep 17 00:00:00 2001 From: Stefano Franz Date: Tue, 23 Oct 2018 16:45:07 +0100 Subject: [PATCH 6/9] ENT-2509 - Make @InitiatedBy flows overridable via node config (#3960) * first attempt at a flowManager fix test breakages add testing around registering subclasses make flowManager a param of MockNode extract interface rename methods more work around overriding flows more test fixes add sample project showing how to use flowOverrides rebase * make smallest possible changes to AttachmentSerializationTest and ReceiveAllFlowTests * add some comments about how flow manager weights flows * address review comments add documentation * address more review comments --- constants.properties | 2 +- .../net/corda/core/contracts/Attachment.kt | 1 - .../core/contracts/AttachmentConstraint.kt | 1 - .../core/internal/cordapp/CordappImpl.kt | 2 +- .../core/transactions/TransactionBuilder.kt | 5 +- .../net/corda/core/flows/FlowTestsUtils.kt | 36 +-- .../corda/core/flows/ReceiveAllFlowTests.kt | 17 +- .../internal/JarSignatureCollectorTest.kt | 1 - .../AttachmentSerializationTest.kt | 15 +- docs/source/flow-overriding.rst | 141 +++++++++++ .../net/corda/node/flows/FlowOverrideTests.kt | 85 +++++++ .../statemachine/FlowVersioningTest.kt | 11 +- .../net/corda/node/internal/AbstractNode.kt | 106 ++------- .../net/corda/node/internal/FlowManager.kt | 222 ++++++++++++++++++ .../node/internal/InitiatedFlowFactory.kt | 2 + .../kotlin/net/corda/node/internal/Node.kt | 19 +- .../cordapp/JarScanningCordappLoader.kt | 24 +- .../node/services/config/NodeConfiguration.kt | 7 +- .../node/internal/FlowRegistrationTest.kt | 42 +++- .../node/internal/NodeFlowManagerTest.kt | 110 +++++++++ .../net/corda/node/internal/NodeTest.kt | 5 +- .../cordapp/JarScanningCordappLoaderTest.kt | 4 +- .../config/NodeConfigurationImplTest.kt | 7 +- .../statemachine/FlowFrameworkTests.kt | 90 ++++--- .../vault/VaultSoftLockManagerTest.kt | 11 +- samples/trader-demo/build.gradle | 14 ++ .../net/corda/traderdemo/flow/BuyerFlow.kt | 36 +-- .../corda/traderdemo/flow/LoggingBuyerFlow.kt | 48 ++++ .../kotlin/net/corda/testing/driver/Driver.kt | 3 +- .../net/corda/testing/driver/DriverDSL.kt | 26 +- .../testing/driver/internal/DriverInternal.kt | 7 +- .../net/corda/testing/node/MockNetwork.kt | 34 ++- .../testing/node/internal/DriverDSLImpl.kt | 26 +- .../node/internal/InternalMockNetwork.kt | 80 +++++-- .../testing/node/internal/NodeBasedTest.kt | 12 +- .../testing/internal/MockCordappProvider.kt | 2 - 36 files changed, 930 insertions(+), 324 deletions(-) create mode 100644 docs/source/flow-overriding.rst create mode 100644 node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/FlowManager.kt create mode 100644 node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt create mode 100644 samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt diff --git a/constants.properties b/constants.properties index 8fde90cd02..9ed2417171 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=4.0.32 +gradlePluginsVersion=4.0.33 kotlinVersion=1.2.71 # ***************************************************************# # When incrementing platformVersion make sure to update # diff --git a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt index d17d053e1f..0535f051e6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Attachment.kt @@ -1,7 +1,6 @@ package net.corda.core.contracts import net.corda.core.KeepForDJVM -import net.corda.core.identity.Party import net.corda.core.internal.extractFile import net.corda.core.serialization.CordaSerializable import java.io.FileNotFoundException diff --git a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt index 76ffc8a866..55be99325b 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/AttachmentConstraint.kt @@ -3,7 +3,6 @@ package net.corda.core.contracts import net.corda.core.DoNotImplement import net.corda.core.KeepForDJVM import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint.isSatisfiedBy -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy import net.corda.core.internal.AttachmentWithContext diff --git a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt index 8ab16a5190..2d6075a5e3 100644 --- a/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt +++ b/core/src/main/kotlin/net/corda/core/internal/cordapp/CordappImpl.kt @@ -43,7 +43,7 @@ data class CordappImpl( */ override val cordappClasses: List = run { val classList = rpcFlows + initiatedFlows + services + serializationWhitelists.map { javaClass } + notaryService - classList.mapNotNull { it?.name } + contractClassNames + classList.mapNotNull { it?.name } + contractClassNames } // TODO Why a seperate Info class and not just have the fields directly in CordappImpl? diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 99c069de88..e52bda5456 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -5,7 +5,10 @@ import net.corda.core.CordaInternal import net.corda.core.DeleteForDJVM import net.corda.core.contracts.* import net.corda.core.cordapp.CordappProvider -import net.corda.core.crypto.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignableData +import net.corda.core.crypto.SignatureMetadata import net.corda.core.identity.Party import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.ensureMinimumPlatformVersion diff --git a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt index fbb2d3bad2..ea3b44a98b 100644 --- a/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt +++ b/core/src/test/kotlin/net/corda/core/flows/FlowTestsUtils.kt @@ -1,10 +1,13 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.core.concurrent.CordaFuture +import net.corda.core.toFuture import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.node.internal.InitiatedFlowFactory import net.corda.testing.node.internal.TestStartedNode +import rx.Observable import kotlin.reflect.KClass /** @@ -34,20 +37,6 @@ class NoAnswer(private val closure: () -> Unit = {}) : FlowLogic() { override fun call() = closure() } -/** - * Allows to register a flow of type [R] against an initiating flow of type [I]. - */ -inline fun , reified R : FlowLogic<*>> TestStartedNode.registerInitiatedFlow(initiatingFlowType: KClass, crossinline construct: (session: FlowSession) -> R) { - registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> construct(session) }, R::class.javaObjectType, true) -} - -/** - * Allows to register a flow of type [Answer] against an initiating flow of type [I], returning a valure of type [R]. - */ -inline fun , reified R : Any> TestStartedNode.registerAnswer(initiatingFlowType: KClass, value: R) { - registerFlowFactory(initiatingFlowType.java, InitiatedFlowFactory.Core { session -> Answer(session, value) }, Answer::class.javaObjectType, true) -} - /** * Extracts data from a [Map[FlowSession, UntrustworthyData]] without performing checks and casting to [R]. */ @@ -112,4 +101,23 @@ inline fun FlowLogic<*>.receiveAll(session: FlowSession, varar private fun Array>>.enforceNoDuplicates() { require(this.size == this.toSet().size) { "A flow session can only appear once as argument." } +} + +inline fun > TestStartedNode.registerCordappFlowFactory( + initiatingFlowClass: KClass>, + initiatedFlowVersion: Int = 1, + noinline flowFactory: (FlowSession) -> P): CordaFuture

{ + + val observable = internals.registerInitiatedFlowFactory( + initiatingFlowClass.java, + P::class.java, + InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), + track = true) + return observable.toFuture() +} + +fun > TestStartedNode.registerCoreFlowFactory(initiatingFlowClass: Class>, + initiatedFlowClass: Class, + flowFactory: (FlowSession) -> T , track: Boolean): Observable { + return this.internals.registerInitiatedFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory), track) } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt index c546a06c20..ec56bb1237 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ReceiveAllFlowTests.kt @@ -2,16 +2,18 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import com.natpryce.hamkrest.assertion.assert -import net.corda.testing.internal.matchers.flow.willReturn import net.corda.core.flows.mixins.WithMockNet import net.corda.core.identity.Party import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.unwrap import net.corda.testing.core.singleIdentity +import net.corda.testing.internal.matchers.flow.willReturn import net.corda.testing.node.internal.InternalMockNetwork +import net.corda.testing.node.internal.TestStartedNode import org.assertj.core.api.Assertions.assertThat import org.junit.AfterClass import org.junit.Test +import kotlin.reflect.KClass class ReceiveMultipleFlowTests : WithMockNet { @@ -43,7 +45,7 @@ class ReceiveMultipleFlowTests : WithMockNet { } } - nodes[1].registerInitiatedFlow(initiatingFlow::class) { session -> + nodes[1].registerCordappFlowFactory(initiatingFlow::class) { session -> object : FlowLogic() { @Suspendable override fun call() { @@ -123,4 +125,15 @@ class ReceiveMultipleFlowTests : WithMockNet { return double * string.length } } +} + +private inline fun TestStartedNode.registerAnswer(kClass: KClass>, value1: T) { + this.registerCordappFlowFactory(kClass) { session -> + object : FlowLogic() { + @Suspendable + override fun call() { + session.send(value1!!) + } + } + } } \ No newline at end of file diff --git a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt index 7f83f23a47..1a379a9f09 100644 --- a/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt +++ b/core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt @@ -8,7 +8,6 @@ import net.corda.core.JarSignatureTestUtils.updateJar import net.corda.core.identity.Party import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME -import net.corda.testing.core.CHARLIE_NAME import org.assertj.core.api.Assertions.assertThat import org.junit.After import org.junit.AfterClass diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index 3ba769ce88..c89e1d0c17 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -3,16 +3,12 @@ package net.corda.core.serialization import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Attachment import net.corda.core.crypto.SecureHash -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowSession -import net.corda.core.flows.InitiatingFlow -import net.corda.core.flows.TestNoSecurityDataVendingFlow +import net.corda.core.flows.* import net.corda.core.identity.Party import net.corda.core.internal.FetchAttachmentsFlow import net.corda.core.internal.FetchDataFlow import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.persistence.NodeAttachmentService import net.corda.nodeapi.internal.persistence.currentDBSession import net.corda.testing.core.ALICE_NAME @@ -151,11 +147,10 @@ class AttachmentSerializationTest { } private fun launchFlow(clientLogic: ClientLogic, rounds: Int, sendData: Boolean = false) { - server.registerFlowFactory( - ClientLogic::class.java, - InitiatedFlowFactory.Core { ServerLogic(it, sendData) }, - ServerLogic::class.java, - track = false) + server.registerCordappFlowFactory( + ClientLogic::class, + 1 + ) { ServerLogic(it, sendData) } client.services.startFlow(clientLogic) mockNet.runNetwork(rounds) } diff --git a/docs/source/flow-overriding.rst b/docs/source/flow-overriding.rst new file mode 100644 index 0000000000..273ff7fa03 --- /dev/null +++ b/docs/source/flow-overriding.rst @@ -0,0 +1,141 @@ +Configuring Responder Flows +=========================== + +A flow can be a fairly complex thing that interacts with many backend systems, and so it is likely that different users +of a specific CordApp will require differences in how flows interact with their specific infrastructure. + +Corda supports this functionality by providing two mechanisms to modify the behaviour of apps in your node. + +Subclassing a Flow +------------------ + +If you have a workflow which is mostly common, but also requires slight alterations in specific situations, most developers would be familiar +with refactoring into `Base` and `Sub` classes. A simple example is shown below. + +java +~~~~ + + .. code-block:: java + + @InitiatingFlow + public class Initiator extends FlowLogic { + private final Party otherSide; + + public Initiator(Party otherSide) { + this.otherSide = otherSide; + } + + @Override + public String call() throws FlowException { + return initiateFlow(otherSide).receive(String.class).unwrap((it) -> it); + } + } + + @InitiatedBy(Initiator.class) + public class BaseResponder extends FlowLogic { + private FlowSession counterpartySession; + + public BaseResponder(FlowSession counterpartySession) { + super(); + this.counterpartySession = counterpartySession; + } + + @Override + public Void call() throws FlowException { + counterpartySession.send(getMessage()); + return Void; + } + + + protected String getMessage() { + return "This Is the Legacy Responder"; + } + } + + public class SubResponder extends BaseResponder { + + public SubResponder(FlowSession counterpartySession) { + super(counterpartySession); + } + + @Override + protected String getMessage() { + return "This is the sub responder"; + } + } + + + +kotlin +~~~~~~ + + .. code-block:: kotlin + + @InitiatedBy(Initiator::class) + open class BaseResponder(internal val otherSideSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + otherSideSession.send(getMessage()) + } + protected open fun getMessage() = "This Is the Legacy Responder" + } + + @InitiatedBy(Initiator::class) + class SubResponder(otherSideSession: FlowSession) : BaseResponder(otherSideSession) { + override fun getMessage(): String { + return "This is the sub responder" + } + } + + + + + +Corda would detect that both ``BaseResponder`` and ``SubResponder`` are configured for responding to ``Initiator``. +Corda will then calculate the hops to ``FlowLogic`` and select the implementation which is furthest distance, ie: the most subclassed implementation. +In the above example, ``SubResponder`` would be selected as the default responder for ``Initiator`` + +.. note:: The flows do not need to be within the same CordApp, or package, therefore to customise a shared app you obtained from a third party, you'd write your own CorDapp that subclasses the first." + +Overriding a flow via node configuration +---------------------------------------- + +Whilst the subclassing approach is likely to be useful for most applications, there is another mechanism to override this behaviour. +This would be useful if for example, a specific CordApp user requires such a different responder that subclassing an existing flow +would not be a good solution. In this case, it's possible to specify a hardcoded flow via the node configuration. + +The configuration section is named ``flowOverrides`` and it accepts an array of ``overrides`` + +.. container:: codeset + + .. code-block:: json + + flowOverrides { + overrides=[ + { + initiator="net.corda.Initiator" + responder="net.corda.BaseResponder" + } + ] + } + +The cordform plugin also provides a ``flowOverride`` method within the ``deployNodes`` block which can be used to override a flow. In the below example, we will override +the ``SubResponder`` with ``BaseResponder`` + +.. container:: codeset + + .. code-block:: groovy + + node { + name "O=Bank,L=London,C=GB" + p2pPort 10025 + rpcUsers = ext.rpcUsers + rpcSettings { + address "localhost:10026" + adminAddress "localhost:10027" + } + extraConfig = ['h2Settings.address' : 'localhost:10035'] + flowOverride("net.corda.Initiator", "net.corda.BaseResponder") + } + +This will generate the corresponding ``flowOverrides`` section and place it in the configuration for that node. \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt new file mode 100644 index 0000000000..881daf18cf --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt @@ -0,0 +1,85 @@ +package net.corda.node.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.unwrap +import net.corda.testing.core.singleIdentity +import net.corda.testing.driver.DriverParameters +import net.corda.testing.driver.driver +import net.corda.testing.node.internal.cordappForClasses +import org.hamcrest.CoreMatchers.`is` +import org.junit.Assert +import org.junit.Test + +class FlowOverrideTests { + + @StartableByRPC + @InitiatingFlow + class Ping(private val pongParty: Party) : FlowLogic() { + @Suspendable + override fun call(): String { + val pongSession = initiateFlow(pongParty) + return pongSession.sendAndReceive("PING").unwrap { it } + } + } + + @InitiatedBy(Ping::class) + open class Pong(private val pingSession: FlowSession) : FlowLogic() { + companion object { + val PONG = "PONG" + } + + @Suspendable + override fun call() { + pingSession.send(PONG) + } + } + + @InitiatedBy(Ping::class) + class Pong2(private val pingSession: FlowSession) : FlowLogic() { + @Suspendable + override fun call() { + pingSession.send("PONGPONG") + } + } + + @InitiatedBy(Ping::class) + class Pongiest(private val pingSession: FlowSession) : Pong(pingSession) { + + companion object { + val GORGONZOLA = "Gorgonzola" + } + + @Suspendable + override fun call() { + pingSession.send(GORGONZOLA) + } + } + + private val nodeAClasses = setOf(Ping::class.java, + Pong::class.java, Pongiest::class.java) + private val nodeBClasses = setOf(Ping::class.java, Pong::class.java) + + @Test + fun `should use the most "specific" implementation of a responding flow`() { + driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { + val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow() + val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() + Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pongiest.GORGONZOLA)) + } + } + + @Test + fun `should use the overriden implementation of a responding flow`() { + val flowOverrides = mapOf(Ping::class.java to Pong::class.java) + driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { + val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray())), flowOverrides = flowOverrides).getOrThrow() + val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() + Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pong.PONG)) + } + } + +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt index f4b3531b13..1248d75b4f 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/statemachine/FlowVersioningTest.kt @@ -7,10 +7,11 @@ import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.testing.core.singleIdentity -import net.corda.testing.node.internal.NodeBasedTest +import net.corda.node.internal.NodeFlowManager import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME +import net.corda.testing.core.singleIdentity +import net.corda.testing.node.internal.NodeBasedTest import net.corda.testing.node.internal.startFlow import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -18,9 +19,10 @@ import org.junit.Test class FlowVersioningTest : NodeBasedTest() { @Test fun `getFlowContext returns the platform version for core flows`() { + val bobFlowManager = NodeFlowManager() val alice = startNode(ALICE_NAME, platformVersion = 2) - val bob = startNode(BOB_NAME, platformVersion = 3) - bob.node.installCoreFlow(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow) + val bob = startNode(BOB_NAME, platformVersion = 3, flowManager = bobFlowManager) + bobFlowManager.registerInitiatedCoreFlowFactory(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow) val (alicePlatformVersionAccordingToBob, bobPlatformVersionAccordingToAlice) = alice.services.startFlow( PretendInitiatingCoreFlow(bob.info.singleIdentity())).resultFuture.getOrThrow() assertThat(alicePlatformVersionAccordingToBob).isEqualTo(2) @@ -45,4 +47,5 @@ class FlowVersioningTest : NodeBasedTest() { @Suspendable override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion) } + } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index d8b9aa15a6..2d12f73a9e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -30,7 +30,10 @@ import net.corda.core.schemas.MappedSchema import net.corda.core.serialization.SerializationWhitelist import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.core.utilities.* +import net.corda.core.utilities.NetworkHostAndPort +import net.corda.core.utilities.days +import net.corda.core.utilities.getOrThrow +import net.corda.core.utilities.minutes import net.corda.node.CordaClock import net.corda.node.SerialFilter import net.corda.node.VersionInfo @@ -99,13 +102,10 @@ import java.time.Clock import java.time.Duration import java.time.format.DateTimeParseException import java.util.* -import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit.MINUTES import java.util.concurrent.TimeUnit.SECONDS -import kotlin.collections.set -import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair /** @@ -120,11 +120,11 @@ abstract class AbstractNode(val configuration: NodeConfiguration, val platformClock: CordaClock, cacheFactoryPrototype: BindableNamedCacheFactory, protected val versionInfo: VersionInfo, + protected val flowManager: FlowManager, protected val serverThread: AffinityExecutor.ServiceAffinityExecutor, private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() { protected abstract val log: Logger - @Suppress("LeakingThis") private var tokenizableServices: MutableList? = mutableListOf(platformClock, this) @@ -211,7 +211,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration, ).tokenize().closeOnStop() private val cordappServices = MutableClassToInstanceMap.create() - private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() private val shutdownExecutor = Executors.newSingleThreadExecutor() protected abstract val transactionVerifierWorkerCount: Int @@ -237,7 +236,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration, private var _started: S? = null private fun T.tokenize(): T { - tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised") + tokenizableServices?.add(this) + ?: throw IllegalStateException("The tokenisable services list has already been finalised") return this } @@ -607,91 +607,27 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } private fun registerCordappFlows() { - cordappLoader.cordapps.flatMap { it.initiatedFlows } - .forEach { + cordappLoader.cordapps.forEach { cordapp -> + cordapp.initiatedFlows.groupBy { it.requireAnnotation().value.java }.forEach { initiator, responders -> + responders.forEach { responder -> try { - registerInitiatedFlowInternal(smm, it, track = false) + flowManager.registerInitiatedFlow(initiator, responder) } catch (e: NoSuchMethodException) { - log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " + + log.error("${responder.name}, as an initiated flow, must have a constructor with a single parameter " + "of type ${Party::class.java.name}") - } catch (e: Exception) { - log.error("Unable to register initiated flow ${it.name}", e) + throw e } } - } - - fun > registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class): Observable { - return registerInitiatedFlowInternal(smm, initiatedFlowClass, track = true) - } - - // TODO remove once not needed - private fun deprecatedFlowConstructorMessage(flowClass: Class<*>): String { - return "Installing flow factory for $flowClass accepting a ${Party::class.java.simpleName}, which is deprecated. " + - "It should accept a ${FlowSession::class.java.simpleName} instead" - } - - private fun > registerInitiatedFlowInternal(smm: StateMachineManager, initiatedFlow: Class, track: Boolean): Observable { - val constructors = initiatedFlow.declaredConstructors.associateBy { it.parameterTypes.toList() } - val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true } - val ctor: (FlowSession) -> F = if (flowSessionCtor == null) { - // Try to fallback to a Party constructor - val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true } - if (partyCtor == null) { - throw IllegalArgumentException("$initiatedFlow must have a constructor accepting a ${FlowSession::class.java.name}") - } else { - log.warn(deprecatedFlowConstructorMessage(initiatedFlow)) } - { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } - } else { - { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } } - val initiatingFlow = initiatedFlow.requireAnnotation().value.java - val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass - require(classWithAnnotation == initiatingFlow) { - "${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiatingFlow.name}" - } - val flowFactory = InitiatedFlowFactory.CorDapp(version, initiatedFlow.appName, ctor) - val observable = internalRegisterFlowFactory(smm, initiatingFlow, flowFactory, initiatedFlow, track) - log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)") - return observable - } - - protected fun > internalRegisterFlowFactory(smm: StateMachineManager, - initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable { - val observable = if (track) { - smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) - } else { - Observable.empty() - } - check(initiatingFlowClass !in flowFactories.keys) { - "$initiatingFlowClass is attempting to register multiple initiated flows" - } - flowFactories[initiatingFlowClass] = flowFactory - return observable - } - - /** - * Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using - * [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards - * compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party. - */ - @VisibleForTesting - fun installCoreFlow(clientFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) { - require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) { - "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" - } - flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory) - log.debug { "Installed core flow ${clientFlowClass.java.name}" } + flowManager.validateRegistrations() } private fun installCoreFlows() { - installCoreFlow(FinalityFlow::class, ::FinalityHandler) - installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler) - installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler) - installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler) + flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler) + flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler) + flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler) + flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler) } protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage { @@ -781,7 +717,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, service.run { tokenize() runOnStop += ::stop - installCoreFlow(NotaryFlow.Client::class, ::createServiceFlow) + flowManager.registerInitiatedCoreFlowFactory(NotaryFlow.Client::class, ::createServiceFlow) start() } return service @@ -961,7 +897,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, } override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { - return flowFactories[initiatingFlowClass] + return flowManager.getFlowFactoryForInitiatingFlow(initiatingFlowClass) } override fun jdbcSession(): Connection = database.createSession() @@ -1066,4 +1002,4 @@ fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSsl } // Here we're using the node's RPC key store as the RPC client's trust store. return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword) -} +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt b/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt new file mode 100644 index 0000000000..68aa8ff056 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/FlowManager.kt @@ -0,0 +1,222 @@ +package net.corda.node.internal + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party +import net.corda.core.internal.uncheckedCast +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.node.internal.classloading.requireAnnotation +import net.corda.node.services.config.FlowOverrideConfig +import net.corda.node.services.statemachine.appName +import net.corda.node.services.statemachine.flowVersionAndInitiatingClass +import javax.annotation.concurrent.ThreadSafe +import kotlin.reflect.KClass + +/** + * + * This class is responsible for organising which flow should respond to a specific @InitiatingFlow + * + * There are two main ways to modify the behaviour of a cordapp with regards to responding with a different flow + * + * 1.) implementing a new subclass. For example, if we have a ResponderFlow similar to @InitiatedBy(Sender) MyBaseResponder : FlowLogic + * If we subclassed a new Flow with specific logic for DB2, it would be similar to IBMB2Responder() : MyBaseResponder + * When these two flows are encountered by the classpath scan for @InitiatedBy, they will both be selected for responding to Sender + * This implementation will sort them for responding in order of their "depth" from FlowLogic - see: FlowWeightComparator + * So IBMB2Responder would win and it would be selected for responding + * + * 2.) It is possible to specify a flowOverride key in the node configuration. Say we configure a node to have + * flowOverrides{ + * "Sender" = "MyBaseResponder" + * } + * In this case, FlowWeightComparator would detect that there is an override in action, and it will assign MyBaseResponder a maximum weight + * This will result in MyBaseResponder being selected for responding to Sender + * + * + */ +interface FlowManager { + + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: (FlowSession) -> FlowLogic<*>) + fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: InitiatedFlowFactory.Core>) + + fun > registerInitiatedFlow(initiator: Class>, responder: Class) + fun > registerInitiatedFlow(responder: Class) + + fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? + + fun validateRegistrations() +} + +@ThreadSafe +open class NodeFlowManager(flowOverrides: FlowOverrideConfig? = null) : FlowManager { + + private val flowFactories = HashMap>, MutableList>() + private val flowOverrides = (flowOverrides + ?: FlowOverrideConfig()).overrides.map { it.initiator to it.responder }.toMutableMap() + + companion object { + private val log = contextLogger() + + } + + @Synchronized + override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? { + return flowFactories[initiatedFlowClass]?.firstOrNull()?.flowFactory + } + + @Synchronized + override fun > registerInitiatedFlow(responder: Class) { + return registerInitiatedFlow(responder.requireAnnotation().value.java, responder) + } + + @Synchronized + override fun > registerInitiatedFlow(initiator: Class>, responder: Class) { + val constructors = responder.declaredConstructors.associateBy { it.parameterTypes.toList() } + val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true } + val ctor: (FlowSession) -> F = if (flowSessionCtor == null) { + // Try to fallback to a Party constructor + val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true } + if (partyCtor == null) { + throw IllegalArgumentException("$responder must have a constructor accepting a ${FlowSession::class.java.name}") + } else { + log.warn("Installing flow factory for $responder accepting a ${Party::class.java.simpleName}, which is deprecated. " + + "It should accept a ${FlowSession::class.java.simpleName} instead") + } + { flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) } + } else { + { flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) } + } + val (version, classWithAnnotation) = initiator.flowVersionAndInitiatingClass + require(classWithAnnotation == initiator) { + "${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiator.name}" + } + val flowFactory = InitiatedFlowFactory.CorDapp(version, responder.appName, ctor) + registerInitiatedFlowFactory(initiator, flowFactory, responder) + log.info("Registered ${initiator.name} to initiate ${responder.name} (version $version)") + } + + private fun > registerInitiatedFlowFactory(initiatingFlowClass: Class>, + flowFactory: InitiatedFlowFactory, + initiatedFlowClass: Class?) { + + check(flowFactory !is InitiatedFlowFactory.Core) { "This should only be used for Cordapp flows" } + val listOfFlowsForInitiator = flowFactories.computeIfAbsent(initiatingFlowClass) { mutableListOf() } + if (listOfFlowsForInitiator.isNotEmpty() && listOfFlowsForInitiator.first().type == FlowType.CORE) { + throw IllegalStateException("Attempting to register over an existing platform flow: $initiatingFlowClass") + } + synchronized(listOfFlowsForInitiator) { + val flowToAdd = RegisteredFlowContainer(initiatingFlowClass, initiatedFlowClass, flowFactory, FlowType.CORDAPP) + val flowWeightComparator = FlowWeightComparator(initiatingFlowClass, flowOverrides) + listOfFlowsForInitiator.add(flowToAdd) + listOfFlowsForInitiator.sortWith(flowWeightComparator) + if (listOfFlowsForInitiator.size > 1) { + log.warn("Multiple flows are registered for InitiatingFlow: $initiatingFlowClass, currently using: ${listOfFlowsForInitiator.first().initiatedFlowClass}") + } + } + + } + + // TODO Harmonise use of these methods - 99% of invocations come from tests. + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: (FlowSession) -> FlowLogic<*>) { + registerInitiatedCoreFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory)) + } + + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, flowFactory: (FlowSession) -> FlowLogic<*>) { + registerInitiatedCoreFlowFactory(initiatingFlowClass, null, InitiatedFlowFactory.Core(flowFactory)) + } + + @Synchronized + override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass>, initiatedFlowClass: KClass>?, flowFactory: InitiatedFlowFactory.Core>) { + require(initiatingFlowClass.java.flowVersionAndInitiatingClass.first == 1) { + "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" + } + flowFactories.computeIfAbsent(initiatingFlowClass.java) { mutableListOf() }.add( + RegisteredFlowContainer( + initiatingFlowClass.java, + initiatedFlowClass?.java, + flowFactory, + FlowType.CORE) + ) + log.debug { "Installed core flow ${initiatingFlowClass.java.name}" } + } + + // To verify the integrity of the current state, it is important that the tip of the responders is a unique weight + // if there are multiple flows with the same weight as the tip, it means that it is impossible to reliably pick one as the responder + private fun validateInvariants(toValidate: List) { + val currentTip = toValidate.first() + val flowWeightComparator = FlowWeightComparator(currentTip.initiatingFlowClass, flowOverrides) + val equalWeightAsCurrentTip = toValidate.map { flowWeightComparator.compare(currentTip, it) to it }.filter { it.first == 0 }.map { it.second } + if (equalWeightAsCurrentTip.size > 1) { + val message = "Unable to determine which flow to use when responding to: ${currentTip.initiatingFlowClass.canonicalName}. ${equalWeightAsCurrentTip.map { it.initiatedFlowClass!!.canonicalName }} are all registered with equal weight." + throw IllegalStateException(message) + } + } + + @Synchronized + override fun validateRegistrations() { + flowFactories.values.forEach { + validateInvariants(it) + } + } + + private enum class FlowType { + CORE, CORDAPP + } + + private data class RegisteredFlowContainer(val initiatingFlowClass: Class>, + val initiatedFlowClass: Class>?, + val flowFactory: InitiatedFlowFactory>, + val type: FlowType) + + // this is used to sort the responding flows in order of "importance" + // the logic is as follows + // IF responder is a specific lambda (like for notary implementations / testing code) always return that responder + // ELSE IF responder is present in the overrides list, always return that responder + // ELSE compare responding flows by their depth from FlowLogic, always return the flow which is most specific (IE, has the most hops to FlowLogic) + private open class FlowWeightComparator(val initiatingFlowClass: Class>, val flowOverrides: Map) : Comparator { + + override fun compare(o1: NodeFlowManager.RegisteredFlowContainer, o2: NodeFlowManager.RegisteredFlowContainer): Int { + if (o1.initiatedFlowClass == null && o2.initiatedFlowClass != null) { + return Int.MIN_VALUE + } + if (o1.initiatedFlowClass != null && o2.initiatedFlowClass == null) { + return Int.MAX_VALUE + } + + if (o1.initiatedFlowClass == null && o2.initiatedFlowClass == null) { + return 0 + } + + val hopsTo1 = calculateHopsToFlowLogic(initiatingFlowClass, o1.initiatedFlowClass!!) + val hopsTo2 = calculateHopsToFlowLogic(initiatingFlowClass, o2.initiatedFlowClass!!) + return hopsTo1.compareTo(hopsTo2) * -1 + } + + private fun calculateHopsToFlowLogic(initiatingFlowClass: Class>, + initiatedFlowClass: Class>): Int { + + val overriddenClassName = flowOverrides[initiatingFlowClass.canonicalName] + return if (overriddenClassName == initiatedFlowClass.canonicalName) { + Int.MAX_VALUE + } else { + var currentClass: Class<*> = initiatedFlowClass + var count = 0 + while (currentClass != FlowLogic::class.java) { + currentClass = currentClass.superclass + count++ + } + count; + } + } + + } +} + +private fun Iterable>.toMutableMap(): MutableMap { + return this.toMap(HashMap()) +} diff --git a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt index 3b86147c4e..9d00e83a28 100644 --- a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt +++ b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt @@ -4,10 +4,12 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSession sealed class InitiatedFlowFactory> { + protected abstract val factory: (FlowSession) -> F fun createFlow(initiatingFlowSession: FlowSession): F = factory(initiatingFlowSession) data class Core>(override val factory: (FlowSession) -> F) : InitiatedFlowFactory() + data class CorDapp>(val flowVersion: Int, val appName: String, override val factory: (FlowSession) -> F) : InitiatedFlowFactory() diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index c6cad69913..27523d9ccb 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -43,6 +43,7 @@ import net.corda.node.services.api.StartedNodeServices import net.corda.node.services.config.* import net.corda.node.services.messaging.* import net.corda.node.services.rpc.ArtemisRpcBroker +import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.utilities.* import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER import net.corda.nodeapi.internal.ShutdownHook @@ -56,7 +57,6 @@ import org.apache.commons.lang.SystemUtils import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger import org.slf4j.LoggerFactory -import rx.Observable import rx.Scheduler import rx.schedulers.Schedulers import java.net.BindException @@ -72,8 +72,7 @@ import kotlin.system.exitProcess class NodeWithInfo(val node: Node, val info: NodeInfo) { val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {} fun dispose() = node.stop() - fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = - node.registerInitiatedFlow(node.smm, initiatedFlowClass) + fun > registerInitiatedFlow(initiatedFlowClass: Class) = node.registerInitiatedFlow(node.smm, initiatedFlowClass) } /** @@ -85,12 +84,14 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) { open class Node(configuration: NodeConfiguration, versionInfo: VersionInfo, private val initialiseSerialization: Boolean = true, + flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides), cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory() ) : AbstractNode( configuration, createClock(configuration), cacheFactoryPrototype, versionInfo, + flowManager, // Under normal (non-test execution) it will always be "1" AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1) ) { @@ -202,7 +203,8 @@ open class Node(configuration: NodeConfiguration, return P2PMessagingClient( config = configuration, versionInfo = versionInfo, - serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port), + serverAddress = configuration.messagingServerAddress + ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port), nodeExecutor = serverThread, database = database, networkMap = networkMapCache, @@ -228,7 +230,8 @@ open class Node(configuration: NodeConfiguration, } val messageBroker = if (!configuration.messagingServerExternal) { - val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port) + val brokerBindAddress = configuration.messagingServerAddress + ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port) ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize) } else { null @@ -442,7 +445,7 @@ open class Node(configuration: NodeConfiguration, }.build().start() } - private fun registerNewRelicReporter (registry: MetricRegistry) { + private fun registerNewRelicReporter(registry: MetricRegistry) { log.info("Registering New Relic JMX Reporter:") val reporter = NewRelicReporter.forRegistry(registry) .name("New Relic Reporter") @@ -504,4 +507,8 @@ open class Node(configuration: NodeConfiguration, log.info("Shutdown complete") } + + fun > registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class) { + this.flowManager.registerInitiatedFlow(initiatedFlowClass) + } } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index ac1badeafe..b1ba5f9f29 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -19,7 +19,6 @@ import net.corda.core.serialization.SerializeAsToken import net.corda.core.utilities.contextLogger import net.corda.node.VersionInfo import net.corda.node.cordapp.CordappLoader -import net.corda.node.internal.classloading.requireAnnotation import net.corda.nodeapi.internal.coreContractClasses import net.corda.serialization.internal.DefaultWhitelist import org.apache.commons.collections4.map.LRUMap @@ -148,17 +147,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: private fun findInitiatedFlows(scanResult: RestrictedScanResult): List>> { return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) - // First group by the initiating flow class in case there are multiple mappings - .groupBy { it.requireAnnotation().value.java } - .map { (initiatingFlow, initiatedFlows) -> - val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow)) - if (sorted.size > 1) { - logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " + - "in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " + - "specific sub-type for registration: ${sorted[0].name}.") - } - sorted[0] - } } private fun Class>.isUserInvokable(): Boolean { @@ -209,17 +197,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: } } - private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { - override fun compare(o1: Class>, o2: Class>): Int { - return when { - o1 == o2 -> 0 - o1.isAssignableFrom(o2) -> 1 - o2.isAssignableFrom(o1) -> -1 - else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " + - "both ${o1.name} and ${o2.name}") - } - } - } + private fun loadClass(className: String, type: KClass): Class? { return try { diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index eab762dcdd..3073d7574a 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -76,6 +76,7 @@ interface NodeConfiguration { val p2pSslOptions: MutualSslConfiguration val cordappDirectories: List + val flowOverrides: FlowOverrideConfig? fun validate(): List @@ -97,6 +98,9 @@ interface NodeConfiguration { } } +data class FlowOverrideConfig(val overrides: List = listOf()) +data class FlowOverride(val initiator: String, val responder: String) + /** * Currently registered JMX Reporters */ @@ -210,7 +214,8 @@ data class NodeConfigurationImpl( override val flowMonitorPeriodMillis: Duration = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS, override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS, override val cordappDirectories: List = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT), - override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA + override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA, + override val flowOverrides: FlowOverrideConfig? ) : NodeConfiguration { companion object { private val logger = loggerFor() diff --git a/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt index e6a9b5fd3e..8b59037248 100644 --- a/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/FlowRegistrationTest.kt @@ -12,7 +12,6 @@ import net.corda.testing.core.singleIdentity import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNodeParameters import net.corda.testing.node.StartedMockNode -import org.assertj.core.api.Assertions.assertThatIllegalStateException import org.junit.After import org.junit.Before import org.junit.Test @@ -39,15 +38,15 @@ class FlowRegistrationTest { } @Test - fun `startup fails when two flows initiated by the same flow are registered`() { + fun `succeeds when a subclass of a flow initiated by the same flow is registered`() { // register the same flow twice to invoke the error without causing errors in other tests - responder.registerInitiatedFlow(Responder::class.java) - assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) } + responder.registerInitiatedFlow(Responder1::class.java) + responder.registerInitiatedFlow(Responder1Subclassed::class.java) } @Test fun `a single initiated flow can be registered without error`() { - responder.registerInitiatedFlow(Responder::class.java) + responder.registerInitiatedFlow(Responder1::class.java) val result = initiator.startFlow(Initiator(responder.info.singleIdentity())) mockNetwork.runNetwork() assertNotNull(result.get()) @@ -63,7 +62,38 @@ class Initiator(val party: Party) : FlowLogic() { } @InitiatedBy(Initiator::class) -private class Responder(val session: FlowSession) : FlowLogic() { +private open class Responder1(val session: FlowSession) : FlowLogic() { + open fun getPayload(): String { + return "whats up" + } + + @Suspendable + override fun call() { + session.receive().unwrap { it } + session.send("What's up") + } +} + +@InitiatedBy(Initiator::class) +private open class Responder2(val session: FlowSession) : FlowLogic() { + open fun getPayload(): String { + return "whats up" + } + + @Suspendable + override fun call() { + session.receive().unwrap { it } + session.send("What's up") + } +} + +@InitiatedBy(Initiator::class) +private class Responder1Subclassed(session: FlowSession) : Responder1(session) { + + override fun getPayload(): String { + return "im subclassed! that's what's up!" + } + @Suspendable override fun call() { session.receive().unwrap { it } diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt new file mode 100644 index 0000000000..25722ef295 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeFlowManagerTest.kt @@ -0,0 +1,110 @@ +package net.corda.node.internal + +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.node.services.config.FlowOverride +import net.corda.node.services.config.FlowOverrideConfig +import org.hamcrest.CoreMatchers.`is` +import org.hamcrest.CoreMatchers.instanceOf +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.lang.IllegalStateException + +private val marker = "This is a special marker" + +class NodeFlowManagerTest { + + @InitiatingFlow + class Init : FlowLogic() { + override fun call() { + TODO("not implemented") + } + } + + @InitiatedBy(Init::class) + open class Resp(val otherSesh: FlowSession) : FlowLogic() { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + class Resp2(val otherSesh: FlowSession) : FlowLogic() { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + open class RespSub(sesh: FlowSession) : Resp(sesh) { + override fun call() { + TODO("not implemented") + } + + } + + @InitiatedBy(Init::class) + class RespSubSub(sesh: FlowSession) : RespSub(sesh) { + override fun call() { + TODO("not implemented") + } + + } + + + @Test(expected = IllegalStateException::class) + fun `should fail to validate if more than one registration with equal weight`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java) + nodeFlowManager.validateRegistrations() + } + + @Test() + fun `should allow registration of flows with different weights`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java) + nodeFlowManager.validateRegistrations() + val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + val flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java))) + } + + @Test() + fun `should allow updating of registered responder at runtime`() { + val nodeFlowManager = NodeFlowManager() + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java) + nodeFlowManager.validateRegistrations() + var factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + var flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java))) + // update + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java) + nodeFlowManager.validateRegistrations() + + factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + Assert.assertThat(flow, `is`(instanceOf(RespSubSub::class.java))) + } + + @Test + fun `should allow an override to be specified`() { + val nodeFlowManager = NodeFlowManager(FlowOverrideConfig(listOf(FlowOverride(Init::class.qualifiedName!!, Resp::class.qualifiedName!!)))) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java) + nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java) + nodeFlowManager.validateRegistrations() + + val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!! + val flow = factory.createFlow(Mockito.mock(FlowSession::class.java)) + + Assert.assertThat(flow, `is`(instanceOf(Resp::class.java))) + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt index 48a266ce31..fd2e1db83f 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -149,7 +149,7 @@ class NodeTest { } } - private fun createConfig(nodeName: CordaX500Name): NodeConfiguration { + private fun createConfig(nodeName: CordaX500Name): NodeConfigurationImpl { val fakeAddress = NetworkHostAndPort("0.1.2.3", 456) return NodeConfigurationImpl( baseDirectory = temporaryFolder.root.toPath(), @@ -167,7 +167,8 @@ class NodeTest { flowTimeout = FlowTimeoutConfiguration(timeout = Duration.ZERO, backoffBase = 1.0, maxRestartCount = 1), rpcSettings = NodeRpcSettings(address = fakeAddress, adminAddress = null, ssl = null), messagingServerAddress = null, - notary = null + notary = null, + flowOverrides = FlowOverrideConfig(listOf()) ) } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 176bbfb5d8..9eba3d64ef 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -56,7 +56,7 @@ class JarScanningCordappLoaderTest { val actualCordapp = loader.cordapps.single() assertThat(actualCordapp.contractClassNames).isEqualTo(listOf(isolatedContractId)) - assertThat(actualCordapp.initiatedFlows.single().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") + assertThat(actualCordapp.initiatedFlows.first().name).isEqualTo("net.corda.finance.contracts.isolated.IsolatedDummyFlow\$Acceptor") assertThat(actualCordapp.rpcFlows).isEmpty() assertThat(actualCordapp.schedulableFlows).isEmpty() assertThat(actualCordapp.services).isEmpty() @@ -74,7 +74,7 @@ class JarScanningCordappLoaderTest { assertThat(loader.cordapps).isNotEmpty val actualCordapp = loader.cordapps.single { !it.initiatedFlows.isEmpty() } - assertThat(actualCordapp.initiatedFlows).first().hasSameClassAs(DummyFlow::class.java) + assertThat(actualCordapp.initiatedFlows.first()).hasSameClassAs(DummyFlow::class.java) assertThat(actualCordapp.rpcFlows).first().hasSameClassAs(DummyRPCFlow::class.java) assertThat(actualCordapp.schedulableFlows).first().hasSameClassAs(DummySchedulableFlow::class.java) } diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt index 27247b8eb5..a9e7da8a92 100644 --- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt @@ -172,8 +172,8 @@ class NodeConfigurationImplTest { val errors = configuration.validate() - assertThat(errors).hasOnlyOneElementSatisfying { - error -> error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously") + assertThat(errors).hasOnlyOneElementSatisfying { error -> + error.contains("Cannot configure both compatibilityZoneUrl and networkServices simultaneously") } } @@ -268,7 +268,8 @@ class NodeConfigurationImplTest { noLocalShell = false, rpcSettings = rpcSettings, crlCheckSoftFail = true, - tlsCertCrlDistPoint = null + tlsCertCrlDistPoint = null, + flowOverrides = FlowOverrideConfig(listOf()) ) } } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 075d324e9f..d1517dc318 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -26,7 +26,6 @@ import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker.Change import net.corda.core.utilities.getOrThrow import net.corda.core.utilities.unwrap -import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.persistence.checkpoints import net.corda.testing.contracts.DummyContract import net.corda.testing.contracts.DummyState @@ -116,7 +115,7 @@ class FlowFrameworkTests { @Test fun `exception while fiber suspended`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } val flow = ReceiveFlow(bob) val fiber = aliceNode.services.startFlow(flow) as FlowStateMachineImpl // Before the flow runs change the suspend action to throw an exception @@ -134,7 +133,7 @@ class FlowFrameworkTests { @Test fun `both sides do a send as their first IO request`() { - bobNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } + bobNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } aliceNode.services.startFlow(PingPongFlow(bob, 10L)) mockNet.runNetwork() @@ -151,7 +150,7 @@ class FlowFrameworkTests { @Test fun `other side ends before doing expected send`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { NoOpFlow() } val resultFuture = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { @@ -161,7 +160,7 @@ class FlowFrameworkTests { @Test fun `receiving unexpected session end before entering sendAndReceive`() { - bobNode.registerFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } + bobNode.registerCordappFlowFactory(WaitForOtherSideEndBeforeSendAndReceive::class) { NoOpFlow() } val sessionEndReceived = Semaphore(0) receivedSessionMessagesObservable().filter { it.message is ExistingSessionMessage && it.message.payload === EndSessionMessage @@ -176,7 +175,7 @@ class FlowFrameworkTests { @Test fun `FlowException thrown on other side`() { - val erroringFlow = bobNode.registerFlowFactory(ReceiveFlow::class) { + val erroringFlow = bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } @@ -240,7 +239,7 @@ class FlowFrameworkTests { } } - bobNode.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } + bobNode.registerCordappFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } val resultFuture = aliceNode.services.startFlow(RetryOnExceptionFlow(bob)).resultFuture mockNet.runNetwork() assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") @@ -248,7 +247,7 @@ class FlowFrameworkTests { @Test fun `serialisation issue in counterparty`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(NonSerialisableData(1), it) } val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java).isThrownBy { @@ -258,7 +257,7 @@ class FlowFrameworkTests { @Test fun `FlowException has non-serialisable object`() { - bobNode.registerFlowFactory(ReceiveFlow::class) { + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } } val result = aliceNode.services.startFlow(ReceiveFlow(bob)).resultFuture @@ -275,7 +274,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand(alice.owningKey)) val stx = aliceNode.services.signInitialTransaction(ptx) - val committerFiber = aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { + val committerFiber = aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) }.map { it.stateMachine }.map { uncheckedCast, FlowStateMachine>(it) } val waiterStx = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture @@ -290,7 +289,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand()) val stx = aliceNode.services.signInitialTransaction(ptx) - aliceNode.registerFlowFactory(WaitingFlows.Waiter::class) { + aliceNode.registerCordappFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } } val waiter = bobNode.services.startFlow(WaitingFlows.Waiter(stx, alice)).resultFuture @@ -307,7 +306,7 @@ class FlowFrameworkTests { .addCommand(dummyCommand(alice.owningKey)) val stx = aliceNode.services.signInitialTransaction(ptx) - aliceNode.registerFlowFactory(VaultQueryFlow::class) { + aliceNode.registerCordappFlowFactory(VaultQueryFlow::class) { WaitingFlows.Committer(it) } val result = bobNode.services.startFlow(VaultQueryFlow(stx, alice)).resultFuture @@ -318,7 +317,7 @@ class FlowFrameworkTests { @Test fun `customised client flow`() { - val receiveFlowFuture = bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } + val receiveFlowFuture = bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it) } aliceNode.services.startFlow(CustomSendFlow("Hello", bob)).resultFuture mockNet.runNetwork() assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") @@ -333,7 +332,7 @@ class FlowFrameworkTests { @Test fun `upgraded initiating flow`() { - bobNode.registerFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } + bobNode.registerCordappFlowFactory(UpgradedFlow::class, initiatedFlowVersion = 1) { InitiatedSendFlow("Old initiated", it) } val result = aliceNode.services.startFlow(UpgradedFlow(bob)).resultFuture mockNet.runNetwork() assertThat(receivedSessionMessages).startsWith( @@ -347,7 +346,7 @@ class FlowFrameworkTests { @Test fun `upgraded initiated flow`() { - bobNode.registerFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } + bobNode.registerCordappFlowFactory(SendFlow::class, initiatedFlowVersion = 2) { UpgradedFlow(it) } val initiatingFlow = SendFlow("Old initiating", bob) val flowInfo = aliceNode.services.startFlow(initiatingFlow).resultFuture mockNet.runNetwork() @@ -387,7 +386,7 @@ class FlowFrameworkTests { @Test fun `single inlined sub-flow`() { - bobNode.registerFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } + bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") @@ -395,7 +394,7 @@ class FlowFrameworkTests { @Test fun `double inlined sub-flow`() { - bobNode.registerFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } + bobNode.registerCordappFlowFactory(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } val result = aliceNode.services.startFlow(SendAndReceiveFlow(bob, "Hello")).resultFuture mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") @@ -403,7 +402,7 @@ class FlowFrameworkTests { @Test fun `non-FlowException thrown on other side`() { - val erroringFlowFuture = bobNode.registerFlowFactory(ReceiveFlow::class) { + val erroringFlowFuture = bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } @@ -507,8 +506,8 @@ class FlowFrameworkTripartyTests { @Test fun `sending to multiple parties`() { - bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } - charlieNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + charlieNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } val payload = "Hello World" aliceNode.services.startFlow(SendFlow(payload, bob, charlie)) mockNet.runNetwork() @@ -538,8 +537,8 @@ class FlowFrameworkTripartyTests { fun `receiving from multiple parties`() { val bobPayload = "Test 1" val charliePayload = "Test 2" - bobNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } - charlieNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(bobPayload, it) } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow(charliePayload, it) } val multiReceiveFlow = ReceiveFlow(bob, charlie).nonTerminating() aliceNode.services.startFlow(multiReceiveFlow) aliceNode.internals.acceptableLiveFiberCountOnStop = 1 @@ -564,8 +563,8 @@ class FlowFrameworkTripartyTests { @Test fun `FlowException only propagated to parent`() { - charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - bobNode.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + bobNode.registerCordappFlowFactory(ReceiveFlow::class) { ReceiveFlow(charlie) } val receivingFiber = aliceNode.services.startFlow(ReceiveFlow(bob)) mockNet.runNetwork() assertThatExceptionOfType(UnexpectedFlowEndException::class.java) @@ -577,9 +576,9 @@ class FlowFrameworkTripartyTests { // Bob will send its payload and then block waiting for the receive from Alice. Meanwhile Alice will move // onto Charlie which will throw the exception val node2Fiber = bobNode - .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } + .registerCordappFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .map { it.stateMachine } - charlieNode.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + charlieNode.registerCordappFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val aliceFiber = aliceNode.services.startFlow(ReceiveFlow(bob, charlie)) as FlowStateMachineImpl mockNet.runNetwork() @@ -630,6 +629,8 @@ class FlowFrameworkPersistenceTests { private lateinit var notaryIdentity: Party private lateinit var alice: Party private lateinit var bob: Party + private lateinit var aliceFlowManager: MockNodeFlowManager + private lateinit var bobFlowManager: MockNodeFlowManager @Before fun start() { @@ -637,8 +638,11 @@ class FlowFrameworkPersistenceTests { cordappsForAllNodes = cordappsForPackages("net.corda.finance.contracts", "net.corda.testing.contracts"), servicePeerAllocationStrategy = RoundRobin() ) - aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME)) - bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME)) + aliceFlowManager = MockNodeFlowManager() + bobFlowManager = MockNodeFlowManager() + + aliceNode = mockNet.createNode(InternalMockNodeParameters(legalName = ALICE_NAME, flowManager = aliceFlowManager)) + bobNode = mockNet.createNode(InternalMockNodeParameters(legalName = BOB_NAME, flowManager = bobFlowManager)) receivedSessionMessagesObservable().forEach { receivedSessionMessages += it } @@ -664,7 +668,7 @@ class FlowFrameworkPersistenceTests { @Test fun `flow restarted just after receiving payload`() { - bobNode.registerFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } + bobNode.registerCordappFlowFactory(SendFlow::class) { InitiatedReceiveFlow(it).nonTerminating() } aliceNode.services.startFlow(SendFlow("Hello", bob)) // We push through just enough messages to get only the payload sent @@ -679,7 +683,7 @@ class FlowFrameworkPersistenceTests { @Test fun `flow loaded from checkpoint will respond to messages from before start`() { - aliceNode.registerFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } + aliceNode.registerCordappFlowFactory(ReceiveFlow::class) { InitiatedSendFlow("Hello", it) } bobNode.services.startFlow(ReceiveFlow(alice).nonTerminating()) // Prepare checkpointed receive flow val restoredFlow = bobNode.restartAndGetRestoredFlow() assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") @@ -694,7 +698,7 @@ class FlowFrameworkPersistenceTests { var sentCount = 0 mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } val charlieNode = mockNet.createNode(InternalMockNodeParameters(legalName = CHARLIE_NAME)) - val secondFlow = charlieNode.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + val secondFlow = charlieNode.registerCordappFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } mockNet.runNetwork() val charlie = charlieNode.info.singleIdentity() @@ -802,23 +806,14 @@ private infix fun TestStartedNode.sent(message: SessionMessage): Pair.to(node: TestStartedNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) { - val isPayloadTransfer: Boolean get() = - message is ExistingSessionMessage && message.payload is DataSessionMessage || - message is InitialSessionMessage && message.firstPayload != null + val isPayloadTransfer: Boolean + get() = + message is ExistingSessionMessage && message.payload is DataSessionMessage || + message is InitialSessionMessage && message.firstPayload != null + override fun toString(): String = "$from sent $message to $to" } -private inline fun > TestStartedNode.registerFlowFactory( - initiatingFlowClass: KClass>, - initiatedFlowVersion: Int = 1, - noinline flowFactory: (FlowSession) -> P): CordaFuture

{ - val observable = registerFlowFactory( - initiatingFlowClass.java, - InitiatedFlowFactory.CorDapp(initiatedFlowVersion, "", flowFactory), - P::class.java, - track = true) - return observable.toFuture() -} private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): InitialSessionMessage { return InitialSessionMessage(SessionId(0), 0, clientFlowClass.java.name, flowVersion, "", payload?.serialize()) @@ -1061,7 +1056,8 @@ private class SendAndReceiveFlow(val otherParty: Party, val payload: Any, val ot constructor(otherPartySession: FlowSession, payload: Any) : this(otherPartySession.counterparty, payload, otherPartySession) @Suspendable - override fun call(): Any = (otherPartySession ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } + override fun call(): Any = (otherPartySession + ?: initiateFlow(otherParty)).sendAndReceive(payload).unwrap { it } } private class InlinedSendFlow(val payload: String, val otherPartySession: FlowSession) : FlowLogic() { @@ -1098,4 +1094,4 @@ private class ExceptionFlow(val exception: () -> E) : FlowLogic communicate(clientLogic: AbstractClientLogic, rebootClient: Boolean): FlowStateMachine { - server.registerFlowFactory(AbstractClientLogic::class.java, InitiatedFlowFactory.Core { ServerLogic(it, serverRunning) }, ServerLogic::class.java, false) + server.registerCoreFlowFactory(AbstractClientLogic::class.java, ServerLogic::class.java, { ServerLogic(it, serverRunning) }, false) client.services.startFlow(clientLogic) while (!serverRunning.get()) mockNet.runNetwork(1) if (rebootClient) { diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index f8ffd4edf3..291f15d988 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -89,6 +89,20 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', nodeTask, } extraConfig = ['h2Settings.address' : 'localhost:10017'] } + + //All other nodes should be using LoggingBuyerFlow as it is a subclass of BuyerFlow + node { + name "O=LoggingBank,L=London,C=GB" + p2pPort 10025 + cordapps = ["$project.group:finance:$corda_release_version"] + rpcUsers = ext.rpcUsers + rpcSettings { + address "localhost:10026" + adminAddress "localhost:10027" + } + extraConfig = ['h2Settings.address' : 'localhost:10035'] + flowOverride("net.corda.traderdemo.flow.SellerFlow", "net.corda.traderdemo.flow.BuyerFlow") + } } task integrationTest(type: Test, dependsOn: []) { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 70cee50154..52a5a7c982 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -11,20 +11,17 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.finance.contracts.CommercialPaper -import net.corda.finance.contracts.getCashBalances import net.corda.finance.flows.TwoPartyTradeFlow -import net.corda.traderdemo.TransactionGraphSearch import java.util.* @InitiatedBy(SellerFlow::class) -class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { +open class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") - override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY) @Suspendable - override fun call() { + override fun call(): SignedTransaction { progressTracker.currentStep = STARTING_BUY // Receive the offered amount and automatically agree to it (in reality this would be a longer negotiation) @@ -43,33 +40,6 @@ class BuyerFlow(private val otherSideSession: FlowSession) : FlowLogic() { println("Purchase complete - we are a happy customer! Final transaction is: " + "\n\n${Emoji.renderIfSupported(tradeTX.tx)}") - logIssuanceAttachment(tradeTX) - logBalance() - } - - private fun logBalance() { - val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" } - println("Remaining balance: ${balances.joinToString()}") - } - - private fun logIssuanceAttachment(tradeTX: SignedTransaction) { - // Find the original CP issuance. - // TODO: This is potentially very expensive, and requires transaction details we may no longer have once - // SGX is enabled. Should be replaced with including the attachment on all transactions involving - // the state. - val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx), - TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, - followInputsOfType = CommercialPaper.State::class.java)) - val cpIssuance = search.call().single() - - // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - - cpIssuance.attachments.first().let { - println(""" - -The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar - -${Emoji.renderIfSupported(cpIssuance)}""") - } + return tradeTX } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt new file mode 100644 index 0000000000..841b96f594 --- /dev/null +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/LoggingBuyerFlow.kt @@ -0,0 +1,48 @@ +package net.corda.traderdemo.flow + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowSession +import net.corda.core.flows.InitiatedBy +import net.corda.core.internal.Emoji +import net.corda.core.transactions.SignedTransaction +import net.corda.finance.contracts.CommercialPaper +import net.corda.finance.contracts.getCashBalances +import net.corda.traderdemo.TransactionGraphSearch + +@InitiatedBy(SellerFlow::class) +class LoggingBuyerFlow(private val otherSideSession: FlowSession) : BuyerFlow(otherSideSession) { + + @Suspendable + override fun call(): SignedTransaction { + val tradeTX = super.call() + logIssuanceAttachment(tradeTX) + logBalance() + return tradeTX + } + + private fun logBalance() { + val balances = serviceHub.getCashBalances().entries.map { "${it.key.currencyCode} ${it.value}" } + println("Remaining balance: ${balances.joinToString()}") + } + + private fun logIssuanceAttachment(tradeTX: SignedTransaction) { + // Find the original CP issuance. + // TODO: This is potentially very expensive, and requires transaction details we may no longer have once + // SGX is enabled. Should be replaced with including the attachment on all transactions involving + // the state. + val search = TransactionGraphSearch(serviceHub.validatedTransactions, listOf(tradeTX.tx), + TransactionGraphSearch.Query(withCommandOfType = CommercialPaper.Commands.Issue::class.java, + followInputsOfType = CommercialPaper.State::class.java)) + val cpIssuance = search.call().single() + + // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. + + cpIssuance.attachments.first().let { + println(""" + +The issuance of the commercial paper came with an attachment. You can find it in the attachments directory: $it.jar + +${Emoji.renderIfSupported(cpIssuance)}""") + } + } +} diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt index 793c6fd8bb..14d7037398 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -148,7 +148,8 @@ data class NodeParameters( val maximumHeapSize: String = "512m", val logLevel: String? = null, val additionalCordapps: Collection = emptySet(), - val regenerateCordappsOnStart: Boolean = false + val regenerateCordappsOnStart: Boolean = false, + val flowOverrides: Map>, Class>> = emptyMap() ) { /** * Helper builder for configuring a [Node] from Java. diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt index bd1bcb464a..6eb2e7a71a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/DriverDSL.kt @@ -2,6 +2,7 @@ package net.corda.testing.driver import net.corda.core.DoNotImplement import net.corda.core.concurrent.CordaFuture +import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.identity.Party import net.corda.core.internal.concurrent.map @@ -27,13 +28,14 @@ interface DriverDSL { * Returns the [NotaryHandle] for the single notary on the network. Throws if there are none or more than one. * @see notaryHandles */ - val defaultNotaryHandle: NotaryHandle get() { - return when (notaryHandles.size) { - 0 -> throw IllegalStateException("There are no notaries defined on the network") - 1 -> notaryHandles[0] - else -> throw IllegalStateException("There is more than one notary defined on the network") + val defaultNotaryHandle: NotaryHandle + get() { + return when (notaryHandles.size) { + 0 -> throw IllegalStateException("There are no notaries defined on the network") + 1 -> notaryHandles[0] + else -> throw IllegalStateException("There is more than one notary defined on the network") + } } - } /** * Returns the identity of the single notary on the network. Throws if there are none or more than one. @@ -47,11 +49,12 @@ interface DriverDSL { * @see defaultNotaryHandle * @see notaryHandles */ - val defaultNotaryNode: CordaFuture get() { - return defaultNotaryHandle.nodeHandles.map { - it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") + val defaultNotaryNode: CordaFuture + get() { + return defaultNotaryHandle.nodeHandles.map { + it.singleOrNull() ?: throw IllegalStateException("Default notary is not a single node") + } } - } /** * Start a node. @@ -110,7 +113,8 @@ interface DriverDSL { startInSameProcess: Boolean? = defaultParameters.startInSameProcess, maximumHeapSize: String = defaultParameters.maximumHeapSize, additionalCordapps: Collection = defaultParameters.additionalCordapps, - regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart + regenerateCordappsOnStart: Boolean = defaultParameters.regenerateCordappsOnStart, + flowOverrides: Map>, Class>> = defaultParameters.flowOverrides ): CordaFuture /** diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt index cf85d45e15..927314827a 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/driver/internal/DriverInternal.kt @@ -14,6 +14,7 @@ import net.corda.testing.driver.OutOfProcess import net.corda.testing.node.User import rx.Observable import java.nio.file.Path +import javax.validation.constraints.NotNull interface NodeHandleInternal : NodeHandle { val configuration: NodeConfiguration @@ -70,7 +71,11 @@ data class InProcessImpl( } override fun close() = stop() - override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = node.registerInitiatedFlow(initiatedFlowClass) + @NotNull + override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable { + node.registerInitiatedFlow(initiatedFlowClass) + return Observable.empty() + } } val InProcess.internalServices: StartedNodeServices get() = services as StartedNodeServices 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 3c3ba57aa5..b3731f27ec 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 @@ -206,11 +206,8 @@ class StartedMockNode private constructor(private val node: TestStartedNode) { fun > registerResponderFlow(initiatingFlowClass: Class>, flowFactory: ResponderFlowFactory, responderFlowClass: Class): CordaFuture = - node.registerFlowFactory( - initiatingFlowClass, - InitiatedFlowFactory.CorDapp(flowVersion = 0, appName = "", factory = flowFactory::invoke), - responderFlowClass, true) - .toFuture() + + node.registerInitiatedFlow(initiatingFlowClass, responderFlowClass).toFuture() } /** @@ -240,13 +237,12 @@ interface ResponderFlowFactory> { */ inline fun > StartedMockNode.registerResponderFlow( initiatingFlowClass: Class>, - noinline flowFactory: (FlowSession) -> F): Future = - registerResponderFlow( - initiatingFlowClass, - object : ResponderFlowFactory { - override fun invoke(flowSession: FlowSession) = flowFactory(flowSession) - }, - F::class.java) + noinline flowFactory: (FlowSession) -> F): Future = registerResponderFlow( + initiatingFlowClass, + object : ResponderFlowFactory { + override fun invoke(flowSession: FlowSession) = flowFactory(flowSession) + }, + F::class.java) /** * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. @@ -302,13 +298,13 @@ open class MockNetwork( constructor(cordappPackages: List, parameters: MockNetworkParameters = MockNetworkParameters()) : this(cordappPackages, defaultParameters = parameters) constructor( - cordappPackages: List, - defaultParameters: MockNetworkParameters = MockNetworkParameters(), - networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, - threadPerNode: Boolean = defaultParameters.threadPerNode, - servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, - notarySpecs: List = defaultParameters.notarySpecs, - networkParameters: NetworkParameters = defaultParameters.networkParameters + cordappPackages: List, + defaultParameters: MockNetworkParameters = MockNetworkParameters(), + networkSendManuallyPumped: Boolean = defaultParameters.networkSendManuallyPumped, + threadPerNode: Boolean = defaultParameters.threadPerNode, + servicePeerAllocationStrategy: InMemoryMessagingNetwork.ServicePeerAllocationStrategy = defaultParameters.servicePeerAllocationStrategy, + notarySpecs: List = defaultParameters.notarySpecs, + networkParameters: NetworkParameters = defaultParameters.networkParameters ) : this(emptyList(), defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters, cordappsForAllNodes = cordappsForPackages(cordappPackages)) private val internalMockNetwork: InternalMockNetwork = InternalMockNetwork(defaultParameters, networkSendManuallyPumped, threadPerNode, servicePeerAllocationStrategy, notarySpecs, networkParameters = networkParameters, cordappsForAllNodes = cordappsForAllNodes) diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index c722058ba3..ad2173c181 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -8,6 +8,7 @@ import com.typesafe.config.ConfigValueFactory import net.corda.client.rpc.internal.createCordaRPCClientWithSslAndClassLoader import net.corda.core.concurrent.CordaFuture import net.corda.core.concurrent.firstOf +import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name import net.corda.core.internal.* import net.corda.core.internal.concurrent.* @@ -37,7 +38,6 @@ import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.network.NodeInfoFilesCopier import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme -import net.corda.testing.node.TestCordapp import net.corda.testing.core.ALICE_NAME import net.corda.testing.core.BOB_NAME import net.corda.testing.core.DUMMY_BANK_A_NAME @@ -50,6 +50,7 @@ import net.corda.testing.internal.setGlobalSerialization import net.corda.testing.internal.stubs.CertificateStoreStubs import net.corda.testing.node.ClusterSpec import net.corda.testing.node.NotarySpec +import net.corda.testing.node.TestCordapp import net.corda.testing.node.User import net.corda.testing.node.internal.DriverDSLImpl.Companion.cordappsInCurrentAndAdditionalPackages import okhttp3.OkHttpClient @@ -213,7 +214,8 @@ class DriverDSLImpl( startInSameProcess: Boolean?, maximumHeapSize: String, additionalCordapps: Collection, - regenerateCordappsOnStart: Boolean + regenerateCordappsOnStart: Boolean, + flowOverrides: Map>, Class>> ): CordaFuture { val p2pAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name @@ -230,7 +232,7 @@ class DriverDSLImpl( return registrationFuture.flatMap { networkMapAvailability.flatMap { // But starting the node proper does require the network map - startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart) + startRegisteredNode(name, it, rpcUsers, verifierType, customOverrides, startInSameProcess, maximumHeapSize, p2pAddress, additionalCordapps, regenerateCordappsOnStart, flowOverrides) } } } @@ -244,7 +246,8 @@ class DriverDSLImpl( maximumHeapSize: String = "512m", p2pAddress: NetworkHostAndPort = portAllocation.nextHostAndPort(), additionalCordapps: Collection = emptySet(), - regenerateCordappsOnStart: Boolean = false): CordaFuture { + regenerateCordappsOnStart: Boolean = false, + flowOverrides: Map>, Class>> = emptyMap()): CordaFuture { val rpcAddress = portAllocation.nextHostAndPort() val rpcAdminAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() @@ -258,14 +261,16 @@ class DriverDSLImpl( "networkServices.networkMapURL" to compatibilityZone.networkMapURL().toString()) } + val flowOverrideConfig = flowOverrides.entries.map { FlowOverride(it.key.canonicalName, it.value.canonicalName) }.let { FlowOverrideConfig(it) } val overrides = configOf( - "myLegalName" to name.toString(), - "p2pAddress" to p2pAddress.toString(), + NodeConfiguration::myLegalName.name to name.toString(), + NodeConfiguration::p2pAddress.name to p2pAddress.toString(), "rpcSettings.address" to rpcAddress.toString(), "rpcSettings.adminAddress" to rpcAdminAddress.toString(), - "useTestClock" to useTestClock, - "rpcUsers" to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, - "verifierType" to verifierType.name + NodeConfiguration::useTestClock.name to useTestClock, + NodeConfiguration::rpcUsers.name to if (users.isEmpty()) defaultRpcUserList else users.map { it.toConfig().root().unwrapped() }, + NodeConfiguration::verifierType.name to verifierType.name, + NodeConfiguration::flowOverrides.name to flowOverrideConfig.toConfig().root().unwrapped() ) + czUrlConfig + customOverrides val config = NodeConfig(ConfigHelper.loadConfig( baseDirectory = baseDirectory(name), @@ -516,8 +521,7 @@ class DriverDSLImpl( localNetworkMap, spec.rpcUsers, spec.verifierType, - customOverrides = notaryConfig(clusterAddress) - ) + customOverrides = notaryConfig(clusterAddress)) // All other nodes will join the cluster val restNodeFutures = nodeNames.drop(1).map { 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 d60f9f6ee1..f280cee72d 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 @@ -30,6 +30,8 @@ import net.corda.core.utilities.seconds import net.corda.node.VersionInfo import net.corda.node.internal.AbstractNode import net.corda.node.internal.InitiatedFlowFactory +import net.corda.node.internal.NodeFlowManager +import net.corda.node.internal.cordapp.JarScanningCordappLoader import net.corda.node.services.api.FlowStarter import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.api.StartedNodeServices @@ -51,7 +53,6 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.network.NetworkParametersCopier import net.corda.nodeapi.internal.persistence.CordaPersistence import net.corda.nodeapi.internal.persistence.DatabaseConfig -import net.corda.testing.node.TestCordapp import net.corda.testing.common.internal.testNetworkParameters import net.corda.testing.internal.rigorousMock import net.corda.testing.internal.setGlobalSerialization @@ -79,7 +80,8 @@ data class MockNodeArgs( val network: InternalMockNetwork, val id: Int, val entropyRoot: BigInteger, - val version: VersionInfo = MOCK_VERSION_INFO + val version: VersionInfo = MOCK_VERSION_INFO, + val flowManager: MockNodeFlowManager = MockNodeFlowManager() ) // TODO We don't need a parameters object as this is internal only @@ -89,7 +91,8 @@ data class InternalMockNodeParameters( val entropyRoot: BigInteger = BigInteger.valueOf(random63BitValue()), val configOverrides: (NodeConfiguration) -> Any? = {}, val version: VersionInfo = MOCK_VERSION_INFO, - val additionalCordapps: Collection? = null) { + val additionalCordapps: Collection? = null, + val flowManager: MockNodeFlowManager = MockNodeFlowManager()) { constructor(mockNodeParameters: MockNodeParameters) : this( mockNodeParameters.forcedID, mockNodeParameters.legalName, @@ -132,12 +135,10 @@ interface TestStartedNode { * starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy]. * @return An [Observable] of the initiated flows started by counterparties. */ - fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable + fun > registerInitiatedFlow(initiatedFlowClass: Class, track: Boolean = false): Observable + + fun > registerInitiatedFlow(initiatingFlowClass: Class>, initiatedFlowClass: Class, track: Boolean = false): Observable - fun > registerFlowFactory(initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable } open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNetworkParameters(), @@ -202,7 +203,8 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe */ val defaultNotaryIdentity: Party get() { - return defaultNotaryNode.info.legalIdentities.singleOrNull() ?: throw IllegalStateException("Default notary has multiple identities") + return defaultNotaryNode.info.legalIdentities.singleOrNull() + ?: throw IllegalStateException("Default notary has multiple identities") } /** @@ -270,11 +272,12 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe } } - open class MockNode(args: MockNodeArgs) : AbstractNode( + open class MockNode(args: MockNodeArgs, private val mockFlowManager: MockNodeFlowManager = args.flowManager) : AbstractNode( args.config, TestClock(Clock.systemUTC()), DefaultNamedCacheFactory(), args.version, + mockFlowManager, args.network.getServerThread(args.id), args.network.busyLatch ) { @@ -294,24 +297,28 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe override val rpcOps: CordaRPCOps, override val notaryService: NotaryService?) : TestStartedNode { - override fun > registerFlowFactory( - initiatingFlowClass: Class>, - flowFactory: InitiatedFlowFactory, - initiatedFlowClass: Class, - track: Boolean): Observable = - internals.internalRegisterFlowFactory(smm, initiatingFlowClass, flowFactory, initiatedFlowClass, track) - override fun dispose() = internals.stop() - override fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable = - internals.registerInitiatedFlow(smm, initiatedFlowClass) + override fun > registerInitiatedFlow(initiatedFlowClass: Class, track: Boolean): Observable { + internals.flowManager.registerInitiatedFlow(initiatedFlowClass) + return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } + + override fun > registerInitiatedFlow(initiatingFlowClass: Class>, initiatedFlowClass: Class, track: Boolean): Observable { + internals.flowManager.registerInitiatedFlow(initiatingFlowClass, initiatedFlowClass) + return smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } + + } val mockNet = args.network val id = args.id + init { require(id >= 0) { "Node ID must be zero or positive, was passed: $id" } } + private val entropyRoot = args.entropyRoot var counter = entropyRoot override val log get() = staticLog @@ -333,7 +340,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe this, attachments, network as MockNodeMessagingService, - object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter { }, + object : StartedNodeServices, ServiceHubInternal by services, FlowStarter by flowStarter {}, nodeInfo, smm, database, @@ -417,8 +424,19 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe var acceptableLiveFiberCountOnStop: Int = 0 override fun acceptableLiveFiberCountOnStop(): Int = acceptableLiveFiberCountOnStop + + fun > registerInitiatedFlowFactory(initiatingFlowClass: Class>, initiatedFlowClass: Class, factory: InitiatedFlowFactory, track: Boolean): Observable { + mockFlowManager.registerTestingFactory(initiatingFlowClass, factory) + return if (track) { + smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } else { + Observable.empty() + } + } } + + fun createUnstartedNode(parameters: InternalMockNodeParameters = InternalMockNodeParameters()): MockNode { return createUnstartedNode(parameters, defaultFactory) } @@ -453,7 +471,7 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe val cordappDirectories = cordapps.map { TestCordappDirectories.getJarDirectory(it) }.distinct() doReturn(cordappDirectories).whenever(config).cordappDirectories - val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version)) + val node = nodeFactory(MockNodeArgs(config, this, id, parameters.entropyRoot, parameters.version, flowManager = parameters.flowManager)) _nodes += node if (start) { node.start() @@ -482,8 +500,10 @@ open class InternalMockNetwork(defaultParameters: MockNetworkParameters = MockNe */ @JvmOverloads fun runNetwork(rounds: Int = -1) { - 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." } + 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) { @@ -572,3 +592,17 @@ private fun mockNodeConfiguration(certificatesDirectory: Path): NodeConfiguratio doReturn(null).whenever(it).devModeOptions } } + +class MockNodeFlowManager : NodeFlowManager() { + val testingRegistrations = HashMap>, InitiatedFlowFactory<*>>() + override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class>): InitiatedFlowFactory<*>? { + if (initiatedFlowClass in testingRegistrations) { + return testingRegistrations.get(initiatedFlowClass) + } + return super.getFlowFactoryForInitiatingFlow(initiatedFlowClass) + } + + fun registerTestingFactory(initiator: Class>, factory: InitiatedFlowFactory<*>) { + testingRegistrations.put(initiator, factory) + } +} \ No newline at end of file diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index dae33f8c4b..a86abe6b9d 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -10,7 +10,9 @@ import net.corda.core.internal.div import net.corda.core.node.NodeInfo import net.corda.core.utilities.getOrThrow import net.corda.node.VersionInfo +import net.corda.node.internal.FlowManager import net.corda.node.internal.Node +import net.corda.node.internal.NodeFlowManager import net.corda.node.internal.NodeWithInfo import net.corda.node.services.config.* import net.corda.nodeapi.internal.config.toConfig @@ -87,7 +89,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi fun startNode(legalName: CordaX500Name, platformVersion: Int = PLATFORM_VERSION, rpcUsers: List = emptyList(), - configOverrides: Map = emptyMap()): NodeWithInfo { + configOverrides: Map = emptyMap(), + flowManager: FlowManager = NodeFlowManager(FlowOverrideConfig())): NodeWithInfo { val baseDirectory = baseDirectory(legalName).createDirectories() val p2pAddress = configOverrides["p2pAddress"] ?: portAllocation.nextHostAndPort().toString() val config = ConfigHelper.loadConfig( @@ -103,7 +106,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi ) + configOverrides ) - val cordapps = cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { cordappPackages + it } ?: cordappPackages) + val cordapps = cordappsForPackages(getCallerPackage(NodeBasedTest::class)?.let { cordappPackages + it } + ?: cordappPackages) val existingCorDappDirectoriesOption = if (config.hasPath(NodeConfiguration.cordappDirectoriesKey)) config.getStringList(NodeConfiguration.cordappDirectoriesKey) else emptyList() @@ -119,7 +123,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } defaultNetworkParameters.install(baseDirectory) - val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion)) + val node = InProcessNode(parsedConfig, MOCK_VERSION_INFO.copy(platformVersion = platformVersion), flowManager = flowManager) val nodeInfo = node.start() val nodeWithInfo = NodeWithInfo(node, nodeInfo) nodes += nodeWithInfo @@ -145,7 +149,7 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } } -class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo) : Node(configuration, versionInfo, false) { +class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) { override fun start() : NodeInfo { check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." } diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt index a6847b8fa2..1494a38715 100644 --- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt +++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt @@ -3,7 +3,6 @@ package net.corda.testing.internal import net.corda.core.contracts.ContractClassName import net.corda.core.cordapp.Cordapp import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party import net.corda.core.internal.DEPLOYED_CORDAPP_UPLOADER import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.node.services.AttachmentId @@ -13,7 +12,6 @@ import net.corda.node.internal.cordapp.CordappProviderImpl import net.corda.testing.services.MockAttachmentStorage import java.nio.file.Paths import java.security.PublicKey -import java.util.* class MockCordappProvider( cordappLoader: CordappLoader, From 7e3aa7f30c01ba83de9dc48c04c8adafdc776eed Mon Sep 17 00:00:00 2001 From: szymonsztuka Date: Wed, 24 Oct 2018 10:53:39 +0100 Subject: [PATCH 7/9] CORDA-1915 node rejects CorDapps signed by our dev keys in prod mode (#4041) Related to CORDA-1915 Signing CorDapp JARs - Corda node rejects CorDapps signed by our development keys when running in production mode. This prevents Cordapps signed by our dev key (by default) running in production (node devMode=false). --- .../core/internal/JarSignatureCollector.kt | 12 ++++++++ .../nodeapi/internal/KeyStoreConfigHelpers.kt | 2 ++ .../net/corda/node/internal/AbstractNode.kt | 5 +++- .../cordapp/JarScanningCordappLoader.kt | 27 ++++++++++++++---- .../cordapp/JarScanningCordappLoaderTest.kt | 22 ++++++++++++++ .../cordapp/signed/signed-by-dev-key.jar | Bin 0 -> 22358 bytes .../cordapp/signed/signed-by-two-keys.jar | Bin 0 -> 24454 bytes 7 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar create mode 100644 node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar diff --git a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt index 963623e474..f7bece5b01 100644 --- a/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt +++ b/core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt @@ -28,6 +28,14 @@ object JarSignatureCollector { fun collectSigningParties(jar: JarInputStream): List = getSigners(jar).toPartiesOrderedByName() + /** + * Returns an ordered list of every [X509Certificate] which has signed every signable item in the given [JarInputStream]. + * + * @param jar The open [JarInputStream] to collect signing parties from. + * @throws InvalidJarSignersException If the signer sets for any two signable items are different from each other. + */ + fun collectCertificates(jar: JarInputStream): List = getSigners(jar).toCertificates() + private fun getSigners(jar: JarInputStream): Set { val signerSets = jar.fileSignerSets if (signerSets.isEmpty()) return emptySet() @@ -71,6 +79,10 @@ object JarSignatureCollector { (it.signerCertPath.certificates[0] as X509Certificate).publicKey }.sortedBy { it.hash} // Sorted for determinism. + private fun Set.toCertificates(): List = map { + it.signerCertPath.certificates[0] as X509Certificate + }.sortedBy { it.toString() } // Sorted for determinism. + private val JarInputStream.entries get(): Sequence = generateSequence(nextJarEntry) { nextJarEntry } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt index 03caed3ca2..d6cb0f5877 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/KeyStoreConfigHelpers.kt @@ -99,6 +99,8 @@ const val DEV_CA_TRUST_STORE_FILE: String = "cordatruststore.jks" const val DEV_CA_TRUST_STORE_PASS: String = "trustpass" const val DEV_CA_TRUST_STORE_PRIVATE_KEY_PASS: String = "trustpasskeypass" +val DEV_CERTIFICATES: List get() = listOf(DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate) + // We need a class so that we can get hold of the class loader internal object DevCaHelper { fun loadDevCa(alias: String): CertificateAndKeyPair { diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 2d12f73a9e..99e9f50778 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -72,6 +72,7 @@ import net.corda.node.services.transactions.SimpleNotaryService import net.corda.node.services.upgrade.ContractUpgradeServiceImpl import net.corda.node.services.vault.NodeVaultService import net.corda.node.utilities.* +import net.corda.nodeapi.internal.DEV_CERTIFICATES import net.corda.nodeapi.internal.NodeInfoAndSigned import net.corda.nodeapi.internal.SignedNodeInfo import net.corda.nodeapi.internal.config.CertificateStore @@ -513,10 +514,12 @@ abstract class AbstractNode(val configuration: NodeConfiguration, // CorDapp will be generated. generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo) } + val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES return JarScanningCordappLoader.fromDirectories( configuration.cordappDirectories, versionInfo, - extraCordapps = generatedCordapps + extraCordapps = generatedCordapps, + blacklistedCerts = blacklistedCerts ) } diff --git a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt index b1ba5f9f29..9bd47c564a 100644 --- a/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt +++ b/node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt @@ -26,6 +26,7 @@ import java.lang.reflect.Modifier import java.net.URL import java.net.URLClassLoader import java.nio.file.Path +import java.security.cert.X509Certificate import java.util.* import java.util.jar.JarInputStream import kotlin.reflect.KClass @@ -38,7 +39,8 @@ import kotlin.streams.toList */ class JarScanningCordappLoader private constructor(private val cordappJarPaths: List, private val versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List) : CordappLoaderTemplate() { + extraCordapps: List, + private val blacklistedCordappSigners: List = emptyList()) : CordappLoaderTemplate() { override val cordapps: List by lazy { loadCordapps() + extraCordapps @@ -64,10 +66,11 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: */ fun fromDirectories(cordappDirs: Collection, versionInfo: VersionInfo = VersionInfo.UNKNOWN, - extraCordapps: List = emptyList()): JarScanningCordappLoader { + extraCordapps: List = emptyList(), + blacklistedCerts: List = emptyList()): JarScanningCordappLoader { logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}") val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } /** @@ -75,9 +78,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: * * @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection. */ - fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList()): JarScanningCordappLoader { + fun fromJarUrls(scanJars: List, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List = emptyList(), blacklistedCerts: List = emptyList()): JarScanningCordappLoader { val paths = scanJars.map { it.restricted() } - return JarScanningCordappLoader(paths, versionInfo, extraCordapps) + return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts) } private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName) @@ -106,6 +109,20 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths: true } } + .filter { + if (blacklistedCordappSigners.isEmpty()) { + true //Nothing blacklisted, no need to check + } else { + val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates) + if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty()) + true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate + else { + logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " + + "${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.") + false + } + } + } cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) } return cordapps } diff --git a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt index 9eba3d64ef..feb4e7b476 100644 --- a/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoaderTest.kt @@ -6,6 +6,7 @@ import net.corda.core.internal.packageName import net.corda.node.VersionInfo import net.corda.testing.node.internal.TestCordappDirectories import net.corda.testing.node.internal.cordappForPackages +import net.corda.nodeapi.internal.DEV_CERTIFICATES import org.assertj.core.api.Assertions.assertThat import org.junit.Test import java.nio.file.Paths @@ -142,4 +143,25 @@ class JarScanningCordappLoaderTest { val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), VersionInfo.UNKNOWN.copy(platformVersion = 2)) assertThat(loader.cordapps).hasSize(1) } + + @Test + fun `cordapp classloader loads app signed by allowed certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = emptyList()) + assertThat(loader.cordapps).hasSize(1) + } + + @Test + fun `cordapp classloader does not load app signed by blacklisted certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-dev-key.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + assertThat(loader.cordapps).hasSize(0) + } + + @Test + fun `cordapp classloader loads app signed by both allowed and non-blacklisted certificate`() { + val jar = JarScanningCordappLoaderTest::class.java.getResource("signed/signed-by-two-keys.jar")!! + val loader = JarScanningCordappLoader.fromJarUrls(listOf(jar), blacklistedCerts = DEV_CERTIFICATES) + assertThat(loader.cordapps).hasSize(1) + } } diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-dev-key.jar new file mode 100644 index 0000000000000000000000000000000000000000..beb401a992b0a44b69c7487d199ed19b43cf885f GIT binary patch literal 22358 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 literal 0 HcmV?d00001 diff --git a/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar b/node/src/test/resources/net/corda/node/internal/cordapp/signed/signed-by-two-keys.jar new file mode 100644 index 0000000000000000000000000000000000000000..c5360a057637c1f66da5139e7e76c94ce5e4b9a2 GIT binary patch literal 24454 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 Date: Wed, 24 Oct 2018 13:58:19 +0100 Subject: [PATCH 8/9] CORDA-1838: Add subcommands to node (#4091) * Tidy up * Add install-shell-extensions command * Make cli tests use same version of picocli as everything else * Remove initLogging from NodeStartup, it is ran earlier by CordaCLIWrapper * Use picocli snapshot for testing * Use RunLast() parser to invoke correct subcommands * Deprecate old clear-network-map-cache parameter * Restructure NodeStartup for commands * Get rid of -c option since the flag method has been deprecated and that didn't exist in last release * Update documentation * Update backwards compatibility test * Get all subcommands working * Refactor sub commands into seperate classes * Update docs and fix some tests * Docs changes * Fix merge conflicts with master * Fix renamed parameters * Fix test failure * Fix compatibility tests * Add missing compatibility test for blob inspector * Remove blob inspector compatibility test as there are import conflicts * Assorted doc fixes * Addressing review comments * More review comments * Couple more bits * Fix broken tests * Fix compilation error * More merge conflicts * Make startup logging function a bit more sensible * Fix broken shell extensions * Make shell extensions work with subcommands * Make sure parameters for deprecated options are carried through * More review comments * Adding some s's * One last go * Fix compilation error on Windows * Revert logging changes * Revert docs back to their original imperatively moody state --- build.gradle | 2 +- docs/source/blob-inspector.rst | 14 +- docs/source/changelog.rst | 6 +- .../cli-application-shell-extensions.rst | 4 +- docs/source/cli-ux-guidelines.rst | 7 +- docs/source/corda-configuration-file.rst | 2 +- docs/source/network-bootstrapper.rst | 13 +- docs/source/network-map.rst | 4 +- docs/source/running-a-node.rst | 31 +- docs/source/shell.rst | 20 +- .../internal/network/NetworkBootstrapper.kt | 2 +- .../net/corda/node/flows/FlowOverrideTests.kt | 2 +- node/src/main/kotlin/net/corda/node/Corda.kt | 4 +- .../net/corda/node/NodeCmdLineOptions.kt | 141 +++--- .../net/corda/node/internal/AbstractNode.kt | 2 +- .../kotlin/net/corda/node/internal/Node.kt | 13 +- .../net/corda/node/internal/NodeStartup.kt | 462 +++++++----------- .../subcommands/ClearNetworkCacheCli.kt | 14 + .../subcommands/GenerateNodeInfoCli.kt | 16 + .../subcommands/GenerateRpcSslCertsCli.kt | 103 ++++ .../subcommands/InitialRegistrationCli.kt | 110 +++++ ...et.corda.node.internal.NodeStartupCli.yml} | 10 - .../internal/NodeStartupCompatibilityTest.kt | 2 +- .../corda/node/internal/NodeStartupTest.kt | 9 +- .../testing/node/internal/DriverDSLImpl.kt | 4 +- .../testing/node/internal/NodeBasedTest.kt | 4 +- testing/test-cli/build.gradle | 2 +- tools/blobinspector/build.gradle | 1 - .../net/corda/blobinspector/BlobInspector.kt | 2 +- .../net.corda.blobinspector.BlobInspector.yml | 58 +++ .../kotlin/net/corda/bootstrapper/Main.kt | 2 +- ...bootstrapper.NetworkBootstrapperRunner.yml | 5 - .../net/corda/cliutils/CordaCliWrapper.kt | 86 ++-- .../cliutils/InstallShellExtensionsParser.kt | 52 +- .../bootstrapper/notaries/NotaryCopier.kt | 2 +- .../net.corda.tools.shell.StandaloneShell.yml | 5 - 36 files changed, 716 insertions(+), 500 deletions(-) create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt create mode 100644 node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt rename node/src/main/resources/{net.corda.node.internal.NodeStartup.yml => net.corda.node.internal.NodeStartupCli.yml} (92%) create mode 100644 tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml diff --git a/build.gradle b/build.gradle index b5c9156435..41f3f98e69 100644 --- a/build.gradle +++ b/build.gradle @@ -70,7 +70,7 @@ buildscript { ext.snappy_version = '0.4' ext.class_graph_version = '4.2.12' ext.jcabi_manifests_version = '1.1' - ext.picocli_version = '3.5.2' + ext.picocli_version = '3.6.1' // Name of the IntelliJ SDK created for the deterministic Java rt.jar. // ext.deterministic_idea_sdk = '1.8 (Deterministic)' diff --git a/docs/source/blob-inspector.rst b/docs/source/blob-inspector.rst index fe58b63a3e..cffa40118b 100644 --- a/docs/source/blob-inspector.rst +++ b/docs/source/blob-inspector.rst @@ -98,9 +98,9 @@ The blob inspector can be started with the following command-line options: .. code-block:: shell - blob-inspector [-hvV] [--full-parties] [--install-shell-extensions] [--schema] - [--format=type] [--input-format=type] - [--logging-level=] [SOURCE] + blob-inspector [-hvV] [--full-parties] [--schema] [--format=type] + [--input-format=type] [--logging-level=] SOURCE + [COMMAND] * ``--format=type``: Output format. Possible values: [YAML, JSON]. Default: YAML. * ``--input-format=type``: Input format. If the file can't be decoded with the given value it's auto-detected, so you should @@ -109,6 +109,10 @@ The blob inspector can be started with the following command-line options: * ``--schema``: Print the blob's schema first. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. -* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file +* ``--version``, ``-V``: Print version information and exit. + +Sub-commands +^^^^^^^^^^^^ + +``install-shell-extensions``: Install ``blob-inspector`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index eb81172b4a..48aca71439 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -1548,9 +1548,9 @@ New features in this release: * Testnet - * Permissioning infrastructure phase one is built out. The node now has a notion of developer mode vs normal - mode. In developer mode it works like M3 and the SSL certificates used by nodes running on your local - machine all self-sign using a developer key included in the source tree. When developer mode is not active, + * Permissioning infrastructure phase one is built out. The node now has a notion of development mode vs normal + mode. In development mode it works like M3 and the SSL certificates used by nodes running on your local + machine all self-sign using a developer key included in the source tree. When development mode is not active, the node won't start until it has a signed certificate. Such a certificate can be obtained by simply running an included command line utility which generates a CSR and submits it to a permissioning service, then waits for the signed certificate to be returned. Note that currently there is no public Corda testnet, so we are diff --git a/docs/source/cli-application-shell-extensions.rst b/docs/source/cli-application-shell-extensions.rst index 4c81ea6c07..ba765d73ed 100644 --- a/docs/source/cli-application-shell-extensions.rst +++ b/docs/source/cli-application-shell-extensions.rst @@ -10,7 +10,7 @@ Users of ``bash`` or ``zsh`` can install an alias and auto-completion for Corda .. code-block:: shell - java -jar .jar --install-shell-extensions + java -jar .jar install-shell-extensions Then, either restart your shell, or for ``bash`` users run: @@ -34,7 +34,7 @@ For example, for the Corda node, install the shell extensions using .. code-block:: shell - java -jar corda-.jar --install-shell-extensions + java -jar corda-.jar install-shell-extensions And then run the node by running: diff --git a/docs/source/cli-ux-guidelines.rst b/docs/source/cli-ux-guidelines.rst index a4917b1620..41b3ac81df 100644 --- a/docs/source/cli-ux-guidelines.rst +++ b/docs/source/cli-ux-guidelines.rst @@ -50,8 +50,11 @@ Standard options * A ``--logging-level`` option should be provided which specifies the logging level to be used in any logging files. Acceptable values should be ``DEBUG``, ``TRACE``, ``INFO``, ``WARN`` and ``ERROR``. * ``--verbose`` and ``--log-to-console`` options should be provided (both equivalent) which specifies that logging output should be displayed in the console. A ``-v`` short option should also be provided. -* A ``--install-shell-extensions`` option should be provided that creates and installs a bash completion file. +Standard subcommands +~~~~~~~~~~~~~~~~~~~~ + +* An ``install-shell-extensions`` subcommand should be provided that creates and installs a bash completion file. Defaults ~~~~~~~~ @@ -94,7 +97,7 @@ In order to use it, create a class containing your command line options using th } class UsefulUtility : CordaCliWrapper( - "useful-utility", // the alias to be used for this utility in bash. When --install-shell-extensions is run + "useful-utility", // the alias to be used for this utility in bash. When install-shell-extensions is run // you will be able to invoke this command by running from the command line "A command line utility that is super useful!" // A description of this utility to be displayed when --help is run ) { diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 6f5fcf17b5..399b0bae78 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -125,7 +125,7 @@ absolute path to the node's base directory. .. note:: The RPC SSL certificate is used by RPC clients to authenticate the connection. The Node operator must provide RPC clients with a truststore containing the certificate they can trust. We advise Node operators to not use the P2P keystore for RPC. - The node ships with a command line argument "--just-generate-rpc-ssl-settings", which generates a secure keystore + The node can be run with the "generate-rpc-ssl-settings" command, which generates a secure keystore and truststore that can be used to secure the RPC connection. You can use this if you have no special requirements. diff --git a/docs/source/network-bootstrapper.rst b/docs/source/network-bootstrapper.rst index 4cc6ec559d..d8568e7ca9 100644 --- a/docs/source/network-bootstrapper.rst +++ b/docs/source/network-bootstrapper.rst @@ -255,14 +255,19 @@ The network bootstrapper can be started with the following command-line options: .. code-block:: shell - bootstrapper [-hvV] [--install-shell-extensions] [--no-copy] [--dir=

] - [--logging-level=] + bootstrapper [-hvV] [--no-copy] [--dir=] [--logging-level=] + [--minimum-platform-version=] [COMMAND] * ``--dir=``: Root directory containing the node configuration files and CorDapp JARs that will form the test network. It may also contain existing node directories. Defaults to the current directory. * ``--no-copy``: Don't copy the CorDapp JARs into the nodes' "cordapps" directories. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. -* ``--version``, ``-V``: Print version information and exit. \ No newline at end of file +* ``--version``, ``-V``: Print version information and exit. +* ``--minimum-platform-version``: The minimum platform version to use in the generated network-parameters. + +Sub-commands +^^^^^^^^^^^^ + +``install-shell-extensions``: Install ``bootstrapper`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. \ No newline at end of file diff --git a/docs/source/network-map.rst b/docs/source/network-map.rst index 3af6b5d6ce..2bda2ed9a8 100644 --- a/docs/source/network-map.rst +++ b/docs/source/network-map.rst @@ -63,7 +63,7 @@ be used to supplement or replace the HTTP network map. If the same node is adver latest one is taken. On startup the node generates its own signed node info file, filename of the format ``nodeInfo-${hash}``. It can also be -generated using the ``--just-generate-node-info`` command line flag without starting the node. To create a simple network +generated using the ``generate-node-info`` sub-command without starting the node. To create a simple network without the HTTP network map service simply place this file in the ``additional-node-infos`` directory of every node that's part of this network. For example, a simple way to do this is to use rsync. @@ -192,7 +192,7 @@ you either need to run from the command line: .. code-block:: shell - java -jar corda.jar --clear-network-map-cache + java -jar corda.jar clear-network-cache or call RPC method `clearNetworkMapCache` (it can be invoked through the node's shell as `run clearNetworkMapCache`, for more information on how to log into node's shell see :doc:`shell`). As we are testing and hardening the implementation this step shouldn't be required. diff --git a/docs/source/running-a-node.rst b/docs/source/running-a-node.rst index 4a214b6845..849139bde9 100644 --- a/docs/source/running-a-node.rst +++ b/docs/source/running-a-node.rst @@ -48,25 +48,38 @@ Command-line options The node can optionally be started with the following command-line options: * ``--base-directory``, ``-b``: The node working directory where all the files are kept (default: ``.``). -* ``--clear-network-map-cache``, ``-c``: Clears local copy of network map, on node startup it will be restored from server or file system. * ``--config-file``, ``-f``: The path to the config file. Defaults to ``node.conf``. -* ``--dev-mode``, ``-d``: Runs the node in developer mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. -* ``--initial-registration``: Start initial node registration with the compatibility zone to obtain a certificate from the Doorman. -* ``--just-generate-node-info``: Perform the node start-up task necessary to generate its nodeInfo, save it to disk, then - quit. -* ``--just-generate-rpc-ssl-settings``: Generate the ssl keystore and truststore for a secure RPC connection. -* ``--network-root-truststore``, ``-t``: Network root trust store obtained from network operator. -* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. +* ``--dev-mode``, ``-d``: Runs the node in development mode. Unsafe in production. Defaults to true on MacOS and desktop versions of Windows. False otherwise. * ``--no-local-shell``, ``-n``: Do not start the embedded shell locally. * ``--on-unknown-config-keys <[FAIL,WARN,INFO]>``: How to behave on unknown node configuration. Defaults to FAIL. * ``--sshd``: Enables SSH server for node administration. * ``--sshd-port``: Sets the port for the SSH server. If not supplied and SSH server is enabled, the port defaults to 2222. * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. +Sub-commands +^^^^^^^^^^^^ + +``bootstrap-raft-cluster``: Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer +addresses), acting as a seed for other nodes to join the cluster. + +``clear-network-cache``: Clears local copy of network map, on node startup it will be restored from server or file system. + +``initial-registration``: Starts initial node registration with the compatibility zone to obtain a certificate from the Doorman. + +Parameters: + +* ``--network-root-truststore``, ``-t`` **required**: Network root trust store obtained from network operator. +* ``--network-root-truststore-password``, ``-p``: Network root trust store password obtained from network operator. + +``generate-node-info``: Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits. + +``generate-rpc-ssl-settings``: Generates the SSL keystore and truststore for a secure RPC connection. + +``install-shell-extensions``: Install ``corda`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + .. _enabling-remote-debugging: Enabling remote debugging diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 43fc87aae5..ee570a48ba 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -105,28 +105,15 @@ Starting the standalone shell Run the following command from the terminal: -Linux and MacOS -^^^^^^^^^^^^^^^ - .. code:: bash - java -jar corda-tools-shell-cli-VERSION_NUMBER.jar [--config-file PATH | --cordpass-directory PATH --commands-directory PATH --host HOST --port PORT - --user USER --password PASSWORD --sshd-port PORT --sshd-hostkey-directory PATH --keystore-password PASSWORD - --keystore-file FILE --truststore-password PASSWORD --truststore-file FILE | --help] - -Windows -^^^^^^^ - -.. code:: bash - - corda-shell [-hvV] [--install-shell-extensions] - [--logging-level=] [--password=] + corda-shell [-hvV] [--logging-level=] [--password=] [--sshd-hostkey-directory=] [--sshd-port=] [--truststore-file=] [--truststore-password=] [--truststore-type=] [--user=] [-a=] [-c=] [-f=] [-o=] - [-p=] + [-p=] [COMMAND] Where: @@ -144,10 +131,11 @@ Where: * ``--truststore-type=``: The type of the TrustStore (e.g. JKS). * ``--verbose``, ``--log-to-console``, ``-v``: If set, prints logging to the console as well as to a file. * ``--logging-level=``: Enable logging at this level and higher. Possible values: ERROR, WARN, INFO, DEBUG, TRACE. Default: INFO. -* ``--install-shell-extensions``: Install ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. * ``--help``, ``-h``: Show this help message and exit. * ``--version``, ``-V``: Print version information and exit. +Additionally, the ``install-shell-extensions`` subcommand can be used to install the ``corda-shell`` alias and auto completion for bash and zsh. See :doc:`cli-application-shell-extensions` for more info. + The format of ``config-file``: .. code:: bash diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt index c581577c57..f9ed9e0f88 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/network/NetworkBootstrapper.kt @@ -65,7 +65,7 @@ internal constructor(private val initSerEnv: Boolean, "java", "-jar", "corda.jar", - "--just-generate-node-info" + "generate-node-info" ) private const val LOGS_DIR_NAME = "logs" diff --git a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt index 881daf18cf..b432de8a53 100644 --- a/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/flows/FlowOverrideTests.kt @@ -64,7 +64,7 @@ class FlowOverrideTests { private val nodeBClasses = setOf(Ping::class.java, Pong::class.java) @Test - fun `should use the most "specific" implementation of a responding flow`() { + fun `should use the most specific implementation of a responding flow`() { driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) { val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow() val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow() diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 62252de5a5..07940ee422 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -4,11 +4,11 @@ package net.corda.node import net.corda.cliutils.start -import net.corda.node.internal.NodeStartup +import net.corda.node.internal.NodeStartupCli fun main(args: Array) { // Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass. // It will exit the process in case of startup failure and is not intended to be used by embedders. If you want // to embed Node in your own container, instantiate it directly and set up the configuration objects yourself. - NodeStartup().start(args) + NodeStartupCli().start(args) } diff --git a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt index 4f4ba98c1a..66be04a34a 100644 --- a/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt +++ b/node/src/main/kotlin/net/corda/node/NodeCmdLineOptions.kt @@ -2,18 +2,18 @@ package net.corda.node import com.typesafe.config.Config import com.typesafe.config.ConfigFactory +import com.typesafe.config.ConfigRenderOptions import net.corda.core.internal.div -import net.corda.core.internal.exists -import net.corda.core.utilities.Try import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.config.NodeConfigurationImpl import net.corda.node.services.config.parseAsNodeConfiguration import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import picocli.CommandLine.Option import java.nio.file.Path import java.nio.file.Paths -class NodeCmdLineOptions { +open class SharedNodeCmdLineOptions { @Option( names = ["-b", "--base-directory"], description = ["The node working directory where all the files are kept."] @@ -27,6 +27,53 @@ class NodeCmdLineOptions { private var _configFile: Path? = null val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf") + @Option( + names = ["--on-unknown-config-keys"], + description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"] + ) + var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL + + @Option( + names = ["-d", "--dev-mode"], + description = ["Runs the node in development mode. Unsafe for production."] + ) + var devMode: Boolean? = null + + open fun loadConfig(): NodeConfiguration { + return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle) + } + + protected fun getRawConfig(): Config { + val rawConfig = ConfigHelper.loadConfig( + baseDirectory, + configFile + ) + if (devMode == true) { + println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") + } + return rawConfig + } + + fun copyFrom(other: SharedNodeCmdLineOptions) { + baseDirectory = other.baseDirectory + _configFile = other._configFile + unknownConfigKeysPolicy= other.unknownConfigKeysPolicy + devMode = other.devMode + } +} + +class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() { + override fun loadConfig(): NodeConfiguration { + return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> + require(!config.devMode) { "Registration cannot occur in development mode" } + require(config.compatibilityZoneURL != null || config.networkServices != null) { + "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." + } + } + } +} + +open class NodeCmdLineOptions : SharedNodeCmdLineOptions() { @Option( names = ["--sshd"], description = ["If set, enables SSH server for node administration."] @@ -45,84 +92,66 @@ class NodeCmdLineOptions { ) var noLocalShell: Boolean = false - @Option( - names = ["--initial-registration"], - description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."] - ) - var isRegistration: Boolean = false - - @Option( - names = ["-t", "--network-root-truststore"], - description = ["Network root trust store obtained from network operator."] - ) - private var _networkRootTrustStorePath: Path? = null - val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks" - - @Option( - names = ["-p", "--network-root-truststore-password"], - description = ["Network root trust store password obtained from network operator."] - ) - var networkRootTrustStorePassword: String? = null - - @Option( - names = ["--on-unknown-config-keys"], - description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"] - ) - var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL - - @Option( - names = ["-d", "--dev-mode"], - description = ["Run the node in developer mode. Unsafe for production."] - ) - var devMode: Boolean? = null - @Option( names = ["--just-generate-node-info"], - description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"] + description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."], + hidden = true ) var justGenerateNodeInfo: Boolean = false @Option( names = ["--just-generate-rpc-ssl-settings"], - description = ["Generate the SSL key and trust stores for a secure RPC connection."] + description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."], + hidden = true ) var justGenerateRpcSslCerts: Boolean = false @Option( - names = ["-c", "--clear-network-map-cache"], - description = ["Clears local copy of network map, on node startup it will be restored from server or file system."] + names = ["--clear-network-map-cache"], + description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."], + hidden = true ) var clearNetworkMapCache: Boolean = false - val nodeRegistrationOption: NodeRegistrationOption? by lazy { - if (isRegistration) { - requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } - require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } - NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!) - } else { - null - } - } + @Option( + names = ["--initial-registration"], + description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."], + hidden = true + ) + var isRegistration: Boolean = false - fun loadConfig(): Pair> { + @Option( + names = ["-t", "--network-root-truststore"], + description = ["DEPRECATED. Network root trust store obtained from network operator."], + hidden = true + ) + var networkRootTrustStorePathParameter: Path? = null + + @Option( + names = ["-p", "--network-root-truststore-password"], + description = ["DEPRECATED. Network root trust store password obtained from network operator."], + hidden = true + ) + var networkRootTrustStorePassword: String? = null + + override fun loadConfig(): NodeConfiguration { val rawConfig = ConfigHelper.loadConfig( baseDirectory, configFile, configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) + if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap() + - if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap()) + if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap()) ) - return rawConfig to Try.on { - rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> - if (nodeRegistrationOption != null) { - require(!config.devMode) { "Registration cannot occur in devMode" } - require(config.compatibilityZoneURL != null || config.networkServices != null) { - "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." - } + return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config -> + if (isRegistration) { + require(!config.devMode) { "Registration cannot occur in development mode" } + require(config.compatibilityZoneURL != null || config.networkServices != null) { + "compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode." } } } } } + data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String) diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index 99e9f50778..aa6b5a16b8 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -661,7 +661,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration, requireNotNull(getCertificateStores()) { "One or more keyStores (identity or TLS) or trustStore not found. " + "Please either copy your existing keys and certificates from another node, " + - "or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " + + "or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " + "Read more at: https://docs.corda.net/permissioning.html" } } catch (e: KeyStoreException) { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index 27523d9ccb..1794cc135c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme +import net.corda.cliutils.ShellConstants import net.corda.core.concurrent.CordaFuture import net.corda.core.flows.FlowLogic import net.corda.core.identity.CordaX500Name @@ -110,9 +111,13 @@ open class Node(configuration: NodeConfiguration, LoggerFactory.getLogger(loggerName).info(msg) } + fun printInRed(message: String) { + println("${ShellConstants.RED}$message${ShellConstants.RESET}") + } + fun printWarning(message: String) { Emoji.renderIfSupported { - println("${Emoji.warningSign} ATTENTION: $message") + printInRed("${Emoji.warningSign} ATTENTION: $message") } staticLog.warn(message) } @@ -132,13 +137,13 @@ open class Node(configuration: NodeConfiguration, // TODO: make this configurable. const val MAX_RPC_MESSAGE_SIZE = 10485760 - fun isValidJavaVersion(): Boolean { + fun isInvalidJavaVersion(): Boolean { if (!hasMinimumJavaVersion()) { println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.") println("Corda will now exit...") - return false + return true } - return true + return false } private fun hasMinimumJavaVersion(): Boolean { 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 f26af3380a..e74de238ca 100644 --- a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -1,31 +1,24 @@ package net.corda.node.internal -import com.typesafe.config.Config import com.typesafe.config.ConfigException -import com.typesafe.config.ConfigRenderOptions import io.netty.channel.unix.Errors -import net.corda.cliutils.CordaCliWrapper -import net.corda.cliutils.CordaVersionProvider -import net.corda.cliutils.ExitCodes +import net.corda.cliutils.* import net.corda.core.crypto.Crypto import net.corda.core.internal.* import net.corda.core.internal.concurrent.thenMatch import net.corda.core.internal.cordapp.CordappImpl import net.corda.core.internal.errors.AddressBindingException import net.corda.core.utilities.Try +import net.corda.core.utilities.contextLogger import net.corda.core.utilities.loggerFor import net.corda.node.* -import net.corda.node.internal.Node.Companion.isValidJavaVersion +import net.corda.node.internal.Node.Companion.isInvalidJavaVersion import net.corda.node.internal.cordapp.MultipleCordappsForFlowException +import net.corda.node.internal.subcommands.* import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.config.shouldStartLocalShell import net.corda.node.services.config.shouldStartSSHDaemon -import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate -import net.corda.node.utilities.registration.HTTPNetworkRegistrationService import net.corda.node.utilities.registration.NodeRegistrationException -import net.corda.node.utilities.registration.NodeRegistrationHelper -import net.corda.node.utilities.saveToKeyStore -import net.corda.node.utilities.saveToTrustStore import net.corda.nodeapi.internal.addShutdownHook import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException @@ -33,10 +26,8 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException import net.corda.tools.shell.InteractiveShell import org.fusesource.jansi.Ansi import org.slf4j.bridge.SLF4JBridgeHandler -import picocli.CommandLine.Mixin +import picocli.CommandLine.* import sun.misc.VMSupport -import java.io.Console -import java.io.File import java.io.IOException import java.io.RandomAccessFile import java.lang.management.ManagementFactory @@ -45,201 +36,145 @@ import java.nio.file.Path import java.time.DayOfWeek import java.time.ZonedDateTime import java.util.* -import kotlin.system.exitProcess -/** This class is responsible for starting a Node from command line arguments. */ -open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { +/** An interface that can be implemented to tell the node what to do once it's intitiated. */ +interface RunAfterNodeInitialisation { + fun run(node: Node) +} + +/** Base class for subcommands to derive from that initialises the logs and provides standard options. */ +abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging { companion object { - private val logger by lazy { loggerFor() } // I guess this is lazy to allow for logging init, but why Node? const val LOGS_DIRECTORY_NAME = "logs" - const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" - private const val INITIAL_REGISTRATION_MARKER = ".initialregistration" + } + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + @Mixin + val cmdLineOptions = SharedNodeCmdLineOptions() +} + +/** Main corda entry point. */ +open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") { + val startup = NodeStartup() + private val networkCacheCli = ClearNetworkCacheCli(startup) + private val justGenerateNodeInfoCli = GenerateNodeInfoCli(startup) + private val justGenerateRpcSslCertsCli = GenerateRpcSslCertsCli(startup) + private val initialRegistrationCli = InitialRegistrationCli(startup) + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli) + + override fun runProgram(): Int { + return when { + InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> { + println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.") + initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions) + initialRegistrationCli.runProgram() + } + //deal with legacy flags and redirect to subcommands + cmdLineOptions.isRegistration -> { + Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.") + requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." } + initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!! + initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter + initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions) + initialRegistrationCli.runProgram() + } + cmdLineOptions.clearNetworkMapCache -> { + Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.") + networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions) + networkCacheCli.runProgram() + } + cmdLineOptions.justGenerateNodeInfo -> { + Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.") + justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions) + justGenerateNodeInfoCli.runProgram() + } + cmdLineOptions.justGenerateRpcSslCerts -> { + Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.") + justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions) + justGenerateRpcSslCertsCli.runProgram() + } + else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + val startupTime = System.currentTimeMillis() + override fun run(node: Node) = startup.startNode(node, startupTime) + }) + } } @Mixin val cmdLineOptions = NodeCmdLineOptions() +} + +/** This class provides a common set of functionality for starting a Node from command line arguments. */ +open class NodeStartup : NodeStartupLogging { + companion object { + private val logger by lazy { loggerFor() } // I guess this is lazy to allow for logging init, but why Node? + const val LOGS_DIRECTORY_NAME = "logs" + const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" + } + + lateinit var cmdLineOptions: SharedNodeCmdLineOptions + + fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int { + this.cmdLineOptions = cmdLineOptions - /** - * @return exit code based on the success of the node startup. This value is intended to be the exit code of the process. - */ - override fun runProgram(): Int { - val startTime = System.currentTimeMillis() // Step 1. Check for supported Java version. - if (!isValidJavaVersion()) return ExitCodes.FAILURE + if (isInvalidJavaVersion()) return ExitCodes.FAILURE // Step 2. We do the single node check before we initialise logging so that in case of a double-node start it // doesn't mess with the running node's logs. enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory) - // Step 3. Initialise logging. - initLogging() - - // Step 4. Register all cryptography [Provider]s. + // Step 3. Register all cryptography [Provider]s. // Required to install our [SecureRandom] before e.g., UUID asks for one. - // This needs to go after initLogging(netty clashes with our logging). + // This needs to go after initLogging(netty clashes with our logging) Crypto.registerProviders() - // Step 5. Print banner and basic node info. + // Step 4. Print banner and basic node info. val versionInfo = getVersionInfo() drawBanner(versionInfo) Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) - // Step 6. Load and validate node configuration. - val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success::value) ?: return ExitCodes.FAILURE + // Step 5. Load and validate node configuration. + val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success::value) + ?: return ExitCodes.FAILURE val errors = configuration.validate() if (errors.isNotEmpty()) { logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}") return ExitCodes.FAILURE } - // Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. - attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE + // Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization. + attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success + ?: return ExitCodes.FAILURE - // Step 8. Any actions required before starting up the Corda network layer. - attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE + // Step 7. Any actions required before starting up the Corda network layer. + attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success + ?: return ExitCodes.FAILURE - // Step 9. Check if in registration mode. - checkAndRunRegistrationMode(configuration, versionInfo)?.let { - return if (it) ExitCodes.SUCCESS - else ExitCodes.FAILURE - } - - // Step 10. Log startup info. + // Step 8. Log startup info. logStartupInfo(versionInfo, configuration) - // Step 11. Start node: create the node, check for other command-line options, add extra logging etc. - attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE + // Step 9. Start node: create the node, check for other command-line options, add extra logging etc. + attempt { + cmdLineOptions.baseDirectory.createDirectories() + afterNodeInitialisation.run(createNode(configuration, versionInfo)) + }.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE return ExitCodes.SUCCESS } - private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? { - checkUnfinishedRegistration() - cmdLineOptions.nodeRegistrationOption?.let { - // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] - attempt { registerWithNetwork(configuration, versionInfo, it) }.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 - } - return null - } - - // TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration. - // There might be cases where the node user should investigate what went wrong before registering again. - private fun checkUnfinishedRegistration() { - if (checkRegistrationMode() && !cmdLineOptions.isRegistration) { - println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") - // Pretend that the node was started with `--initial-registration` to help prevent user error. - cmdLineOptions.isRegistration = true - } - } - - 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) - - private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error) - - private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true - - private val handleRegistrationError = { error: Exception -> - when (error) { - is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") - 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(): NodeConfiguration { - val (rawConfig, configurationResult) = loadConfigFile() - if (cmdLineOptions.devMode == true) { - println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}") - } - return configurationResult.getOrThrow() - } - - private fun checkRegistrationMode(): Boolean { - // If the node was started with `--initial-registration`, create marker file. - // We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails. - val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER - if (!cmdLineOptions.isRegistration && !marker.exists()) { - return false - } - try { - marker.createFile() - } catch (e: Exception) { - logger.warn("Could not create marker file for `--initial-registration`.", e) - } - return true - } - - private fun deleteNodeRegistrationMarker(baseDir: Path) { - try { - val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri()) - if (marker.exists()) { - marker.delete() - } - } catch (e: Exception) { - e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn) - } - } - protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit - protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) + open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo) - protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) { - cmdLineOptions.baseDirectory.createDirectories() - val node = createNode(conf, versionInfo) - if (cmdLineOptions.clearNetworkMapCache) { - node.clearNetworkMapCache() - return - } - if (cmdLineOptions.justGenerateNodeInfo) { - // Perform the minimum required start-up logic to be able to write a nodeInfo to disk - node.generateAndSaveNodeInfo() - return - } - if (cmdLineOptions.justGenerateRpcSslCerts) { - generateRpcSslCertificates(conf) - return - } - - if (conf.devMode) { + fun startNode(node: Node, startTime: Long) { + if (node.configuration.devMode) { Emoji.renderIfSupported { - Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.") + Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.") } } else { logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.") @@ -256,7 +191,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. - if (conf.shouldStartLocalShell()) { + if (node.configuration.shouldStartLocalShell()) { node.startupComplete.then { try { InteractiveShell.runLocalShell(node::stop) @@ -265,8 +200,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } } - if (conf.shouldStartSSHDaemon()) { - Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString()) + if (node.configuration.shouldStartSSHDaemon()) { + Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString()) } }, { th -> @@ -275,82 +210,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { node.run() } - private fun generateRpcSslCertificates(conf: NodeConfiguration) { - val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) - - val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" - val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" - - if (keyStorePath.exists() || trustStorePath.exists()) { - println("Found existing RPC SSL keystores. Command was already run. Exiting..") - exitProcess(0) - } - - val console: Console? = System.console() - - when (console) { - // In this case, the JVM is not connected to the console so we need to exit. - null -> { - println("Not connected to console. Exiting") - exitProcess(1) - } - // Otherwise we can proceed normally. - else -> { - while (true) { - val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ") - // TODO: consider adding a password strength policy. - if (keystorePassword1.isEmpty()) { - println("The RPC keystore password cannot be an empty String.") - continue - } - - val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ") - if (!keystorePassword1.contentEquals(keystorePassword2)) { - println("The RPC keystore passwords don't match.") - continue - } - - saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") - println("The RPC keystore was saved to: $keyStorePath .") - break - } - - while (true) { - val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ") - // TODO: consider adding a password strength policy. - if (trustStorePassword1.isEmpty()) { - println("The RPC truststore password cannot be an empty String.") - continue - } - - val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ") - if (!trustStorePassword1.contentEquals(trustStorePassword2)) { - println("The RPC truststore passwords don't match.") - continue - } - - saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") - println("The RPC truststore was saved to: $trustStorePath .") - println("You need to distribute this file along with the password in a secure way to all RPC clients.") - break - } - - val dollar = '$' - println(""" - | - |The SSL certificates for RPC were generated successfully. - | - |Add this snippet to the "rpcSettings" section of your node.conf: - | useSsl=true - | ssl { - | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks - | keyStorePassword=the_above_password - | } - |""".trimMargin()) - } - } - } - protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) { logger.info("Vendor: ${versionInfo.vendor}") logger.info("Release: ${versionInfo.releaseVersion}") @@ -376,40 +235,12 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { logger.info(nodeStartedMessage) } - protected open fun registerWithNetwork( - conf: NodeConfiguration, - versionInfo: VersionInfo, - nodeRegistrationConfig: NodeRegistrationOption - ) { - println("\n" + - "******************************************************************\n" + - "* *\n" + - "* Registering as a new participant with a Corda network *\n" + - "* *\n" + - "******************************************************************\n") - - NodeRegistrationHelper(conf, - HTTPNetworkRegistrationService( - requireNotNull(conf.networkServices), - versionInfo), - nodeRegistrationConfig).buildKeystore() - - // Minimal changes to make registration tool create node identity. - // TODO: Move node identity generation logic from node to registration helper. - createNode(conf, getVersionInfo()).generateAndSaveNodeInfo() - - println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.") - println("Corda node will now terminate.") - } - - protected open fun loadConfigFile(): Pair> = cmdLineOptions.loadConfig() - protected open fun banJavaSerialisation(conf: NodeConfiguration) { // Note that in dev mode this filter can be overridden by a notary service implementation. SerialFilter.install(::defaultSerialFilter) } - protected open fun getVersionInfo(): VersionInfo { + open fun getVersionInfo(): VersionInfo { return VersionInfo( PLATFORM_VERSION, CordaVersionProvider.releaseVersion, @@ -462,18 +293,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } - override fun initLogging() { - val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) - System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. - if (verbose) { - System.setProperty("consoleLogLevel", loggingLevel) - Node.renderBasicInfoToConsole = false - } - System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) - SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. - SLF4JBridgeHandler.install() - } - private fun lookupMachineNameAndMaybeWarn(): String { val start = System.currentTimeMillis() val hostName: String = InetAddress.getLocalHost().hostName @@ -577,3 +396,66 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") { } } +/** Provide some common logging methods for node startup commands. */ +interface NodeStartupLogging { + companion object { + val logger by lazy { contextLogger() } + val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class) + } + + fun attempt(action: () -> RESULT): Try = Try.on(action) + + fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message) + + fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error) + + fun handleRegistrationError(error: Exception) { + when (error) { + is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}") + else -> error.logAsUnexpected("Exception during node registration") + } + } + + fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true + + fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) } + + fun 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") + } + } + + 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() + } +} + +fun CliWrapperBase.initLogging(baseDirectory: Path) { + val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH) + System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. + if (verbose) { + System.setProperty("consoleLogLevel", loggingLevel) + Node.renderBasicInfoToConsole = false + } + System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString()) + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() +} diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt new file mode 100644 index 0000000000..224697d410 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/ClearNetworkCacheCli.kt @@ -0,0 +1,14 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation { + override fun run(node: Node) = node.clearNetworkMapCache() + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt new file mode 100644 index 0000000000..dc4b240bd8 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateNodeInfoCli.kt @@ -0,0 +1,16 @@ +package net.corda.node.internal.subcommands + +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation + +class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation { + override fun run(node: Node) { + node.generateAndSaveNodeInfo() + } + }) + } +} \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt new file mode 100644 index 0000000000..6174a2df2e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/GenerateRpcSslCertsCli.kt @@ -0,0 +1,103 @@ +package net.corda.node.internal.subcommands + +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.node.internal.Node +import net.corda.node.internal.NodeCliCommand +import net.corda.node.internal.NodeStartup +import net.corda.node.internal.RunAfterNodeInitialisation +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate +import net.corda.node.utilities.saveToKeyStore +import net.corda.node.utilities.saveToTrustStore +import java.io.Console +import kotlin.system.exitProcess + +class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) { + override fun runProgram(): Int { + return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts()) + } +} + +class GenerateRpcSslCerts: RunAfterNodeInitialisation { + override fun run(node: Node) { + generateRpcSslCertificates(node.configuration) + } + + private fun generateRpcSslCertificates(conf: NodeConfiguration) { + val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal) + + val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks" + val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks" + + if (keyStorePath.exists() || trustStorePath.exists()) { + println("Found existing RPC SSL keystores. Command was already run. Exiting.") + exitProcess(0) + } + + val console: Console? = System.console() + + when (console) { + // In this case, the JVM is not connected to the console so we need to exit. + null -> { + println("Not connected to console. Exiting.") + exitProcess(1) + } + // Otherwise we can proceed normally. + else -> { + while (true) { + val keystorePassword1 = console.readPassword("Enter the RPC keystore password:") + // TODO: consider adding a password strength policy. + if (keystorePassword1.isEmpty()) { + println("The RPC keystore password cannot be an empty String.") + continue + } + + val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:") + if (!keystorePassword1.contentEquals(keystorePassword2)) { + println("The RPC keystore passwords don't match.") + continue + } + + saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl") + println("The RPC keystore was saved to: $keyStorePath .") + break + } + + while (true) { + val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:") + // TODO: consider adding a password strength policy. + if (trustStorePassword1.isEmpty()) { + println("The RPC truststore password cannot be an empty string.") + continue + } + + val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:") + if (!trustStorePassword1.contentEquals(trustStorePassword2)) { + println("The RPC truststore passwords don't match.") + continue + } + + saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl") + println("The RPC truststore was saved to: $trustStorePath.") + println("You need to distribute this file along with the password in a secure way to all RPC clients.") + break + } + + val dollar = '$' + println(""" + | + |The SSL certificates for RPC were generated successfully. + | + |Add this snippet to the "rpcSettings" section of your node.conf: + | useSsl=true + | ssl { + | keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks + | keyStorePassword=the_above_password + | } + |""".trimMargin()) + } + } + } + +} diff --git a/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt new file mode 100644 index 0000000000..33c9ac048c --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/subcommands/InitialRegistrationCli.kt @@ -0,0 +1,110 @@ +package net.corda.node.internal.subcommands + +import net.corda.cliutils.CliWrapperBase +import net.corda.core.internal.createFile +import net.corda.core.internal.div +import net.corda.core.internal.exists +import net.corda.core.utilities.Try +import net.corda.node.InitialRegistrationCmdLineOptions +import net.corda.node.NodeRegistrationOption +import net.corda.node.internal.* +import net.corda.node.internal.NodeStartupLogging.Companion.logger +import net.corda.node.services.config.NodeConfiguration +import net.corda.node.utilities.registration.HTTPNetworkRegistrationService +import net.corda.node.utilities.registration.NodeRegistrationHelper +import picocli.CommandLine.Mixin +import picocli.CommandLine.Option +import java.io.File +import java.nio.file.Path + +class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") { + @Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."]) + var networkRootTrustStorePathParameter: Path? = null + + @Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true) + var networkRootTrustStorePassword: String = "" + + override fun runProgram() : Int { + val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks" + return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup)) + } + + override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory) + + @Mixin + val cmdLineOptions = InitialRegistrationCmdLineOptions() +} + +class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging { + companion object { + private const val INITIAL_REGISTRATION_MARKER = ".initialregistration" + + fun checkRegistrationMode(baseDirectory: Path): Boolean { + // If the node was started with `--initial-registration`, create marker file. + // We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails. + val marker = baseDirectory / INITIAL_REGISTRATION_MARKER + if (!marker.exists()) { + return false + } + try { + marker.createFile() + } catch (e: Exception) { + logger.warn("Could not create marker file for `initial-registration`.", e) + } + return true + } + } + + private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword) + + private fun registerWithNetwork(conf: NodeConfiguration) { + val versionInfo = startup.getVersionInfo() + + println("\n" + + "******************************************************************\n" + + "* *\n" + + "* Registering as a new participant with a Corda network *\n" + + "* *\n" + + "******************************************************************\n") + + NodeRegistrationHelper(conf, + HTTPNetworkRegistrationService( + requireNotNull(conf.networkServices), + versionInfo), + nodeRegistration).buildKeystore() + + // Minimal changes to make registration tool create node identity. + // TODO: Move node identity generation logic from node to registration helper. + startup.createNode(conf, versionInfo).generateAndSaveNodeInfo() + + println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.") + println("Corda node will now terminate.") + } + + private fun initialRegistration(config: NodeConfiguration) { + // Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig] + attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success + // At this point the node registration was successful. We can delete the marker file. + deleteNodeRegistrationMarker(baseDirectory) + } + + private fun deleteNodeRegistrationMarker(baseDir: Path) { + try { + val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri()) + if (marker.exists()) { + marker.delete() + } + } catch (e: Exception) { + e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn) + } + } + + override fun run(node: Node) { + require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" } + if (checkRegistrationMode(baseDirectory)) { + println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.") + } + initialRegistration(node.configuration) + } +} + diff --git a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml b/node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml similarity index 92% rename from node/src/main/resources/net.corda.node.internal.NodeStartup.yml rename to node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml index 28f0651a78..ea59505694 100644 --- a/node/src/main/resources/net.corda.node.internal.NodeStartup.yml +++ b/node/src/main/resources/net.corda.node.internal.NodeStartupCli.yml @@ -26,11 +26,6 @@ required: false multiParam: false acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--just-generate-node-info" parameterType: "boolean" required: false @@ -99,11 +94,6 @@ required: false multiParam: true acceptableValues: [] - - parameterName: "-c" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "-d" parameterType: "java.lang.Boolean" required: false diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt index 283814b936..d7766ecb76 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupCompatibilityTest.kt @@ -2,4 +2,4 @@ package net.corda.node.internal import net.corda.testing.CliBackwardsCompatibleTest -class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java) \ No newline at end of file +class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java) \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt index ec1da7a226..976c1fec79 100644 --- a/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt +++ b/node/src/test/kotlin/net/corda/node/internal/NodeStartupTest.kt @@ -1,6 +1,8 @@ package net.corda.node.internal import net.corda.core.internal.div +import net.corda.node.InitialRegistrationCmdLineOptions +import net.corda.node.internal.subcommands.InitialRegistrationCli import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy import org.assertj.core.api.Assertions.assertThat import org.junit.BeforeClass @@ -11,7 +13,7 @@ import java.nio.file.Path import java.nio.file.Paths class NodeStartupTest { - private val startup = NodeStartup() + private val startup = NodeStartupCli() companion object { private lateinit var workingDirectory: Path @@ -30,7 +32,6 @@ class NodeStartupTest { assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf") assertThat(startup.verbose).isEqualTo(false) assertThat(startup.loggingLevel).isEqualTo(Level.INFO) - assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null) assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false) assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false) assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false) @@ -38,7 +39,7 @@ class NodeStartupTest { assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL) assertThat(startup.cmdLineOptions.devMode).isEqualTo(null) assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false) - assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks") + assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null) } @Test @@ -46,6 +47,6 @@ class NodeStartupTest { CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString()) assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir") assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf") - assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks") + assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null) } } diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt index ad2173c181..18bc7da028 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/DriverDSLImpl.kt @@ -322,7 +322,7 @@ class DriverDSLImpl( } else { startOutOfProcessMiniNode( config, - "--initial-registration", + "initial-registration", "--network-root-truststore=${rootTruststorePath.toAbsolutePath()}", "--network-root-truststore-password=$rootTruststorePassword" ).map { config } @@ -457,7 +457,7 @@ class DriverDSLImpl( } else { // TODO The config we use here is uses a hardocded p2p port which changes when the node is run proper // This causes two node info files to be generated. - startOutOfProcessMiniNode(config, "--just-generate-node-info").map { + startOutOfProcessMiniNode(config, "generate-node-info").map { // Once done we have to read the signed node info file that's been generated val nodeInfoFile = config.corda.baseDirectory.list { paths -> paths.filter { it.fileName.toString().startsWith(NodeInfoFilesCopier.NODE_INFO_FILE_NAME_PREFIX) }.findFirst().get() diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt index a86abe6b9d..21988565aa 100644 --- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt +++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/NodeBasedTest.kt @@ -32,6 +32,7 @@ import rx.internal.schedulers.CachedThreadScheduler import java.nio.file.Path import java.util.concurrent.Executors import kotlin.concurrent.thread +import kotlin.test.assertFalse // TODO Some of the logic here duplicates what's in the driver - the reason why it's not straightforward to replace it by // using DriverDSLImpl in `init()` and `stopAllNodes()` is because of the platform version passed to nodes (driver doesn't @@ -150,9 +151,8 @@ abstract class NodeBasedTest(private val cordappPackages: List = emptyLi } class InProcessNode(configuration: NodeConfiguration, versionInfo: VersionInfo, flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides)) : Node(configuration, versionInfo, false, flowManager = flowManager) { - override fun start() : NodeInfo { - check(isValidJavaVersion()) { "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." } + assertFalse(isInvalidJavaVersion(), "You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8." ) return super.start() } diff --git a/testing/test-cli/build.gradle b/testing/test-cli/build.gradle index 462499bf64..3d32512119 100644 --- a/testing/test-cli/build.gradle +++ b/testing/test-cli/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'java' apply plugin: 'kotlin' dependencies { - compile group: 'info.picocli', name: 'picocli', version: '3.0.1' + compile "info.picocli:picocli:$picocli_version" 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" diff --git a/tools/blobinspector/build.gradle b/tools/blobinspector/build.gradle index a1f6f865ef..8c0045035d 100644 --- a/tools/blobinspector/build.gradle +++ b/tools/blobinspector/build.gradle @@ -14,7 +14,6 @@ dependencies { exclude module: 'node-api' exclude module: 'finance' } - testCompile project(':test-utils') } jar { diff --git a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt index 8670989fd1..8d8e762ed7 100644 --- a/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt +++ b/tools/blobinspector/src/main/kotlin/net/corda/blobinspector/BlobInspector.kt @@ -47,7 +47,7 @@ class BlobInspector : CordaCliWrapper("blob-inspector", "Convert AMQP serialised description = ["Display the owningKey and certPath properties of Party and PartyAndReference objects respectively"]) private var fullParties: Boolean = false - @Option(names = ["--schema"], description = ["Print the blob's schema first"]) + @Option(names = ["--schema"], description = ["Prints the blob's schema first"]) private var schema: Boolean = false override fun runProgram() = run(System.out) diff --git a/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml b/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml new file mode 100644 index 0000000000..66b94ae7de --- /dev/null +++ b/tools/blobinspector/src/test/resources/net.corda.blobinspector.BlobInspector.yml @@ -0,0 +1,58 @@ +- commandName: "
" + positionalParams: + - parameterName: "0" + parameterType: "java.net.URL" + required: true + multiParam: false + acceptableValues: [] + params: + - parameterName: "--format" + parameterType: "net.corda.blobinspector.OutputFormatType" + required: false + multiParam: false + acceptableValues: + - "YAML" + - "JSON" + - parameterName: "--full-parties" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--input-format" + parameterType: "net.corda.blobinspector.InputFormatType" + required: false + multiParam: false + acceptableValues: + - "BINARY" + - "HEX" + - "BASE64" + - parameterName: "--log-to-console" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--logging-level" + parameterType: "org.slf4j.event.Level" + required: false + multiParam: false + acceptableValues: + - "ERROR" + - "WARN" + - "INFO" + - "DEBUG" + - "TRACE" + - parameterName: "--schema" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "--verbose" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] + - parameterName: "-v" + parameterType: "boolean" + required: false + multiParam: false + acceptableValues: [] \ No newline at end of file diff --git a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index e3a1967e2e..3436bdfaf3 100644 --- a/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -25,7 +25,7 @@ class NetworkBootstrapperRunner : CordaCliWrapper("bootstrapper", "Bootstrap a l @Option(names = ["--no-copy"], description = ["""Don't copy the CorDapp JARs into the nodes' "cordapps" directories."""]) private var noCopy: Boolean = false - @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters"]) + @Option(names = ["--minimum-platform-version"], description = ["The minimumPlatformVersion to use in the network-parameters."]) private var minimumPlatformVersion = PLATFORM_VERSION override fun runProgram(): Int { diff --git a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml index de9379b953..f8e50ac29b 100644 --- a/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml +++ b/tools/bootstrapper/src/test/resources/net.corda.bootstrapper.NetworkBootstrapperRunner.yml @@ -6,11 +6,6 @@ required: false multiParam: true acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--log-to-console" parameterType: "boolean" required: false diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt index 5a35be8089..60fa089ff4 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/CordaCliWrapper.kt @@ -69,57 +69,43 @@ fun CordaCliWrapper.start(args: Array) { cmd.registerConverter(Path::class.java) { Paths.get(it).toAbsolutePath().normalize() } cmd.commandSpec.name(alias) cmd.commandSpec.usageMessage().description(description) - cmd.commandSpec.parser().collectErrors(true) + this.subCommands().forEach { + val subCommand = CommandLine(it) + it.args = args + subCommand.commandSpec.usageMessage().description(it.description) + cmd.commandSpec.addSubcommand(it.alias, subCommand) + } + try { val defaultAnsiMode = if (CordaSystemUtils.isOsWindows()) { Help.Ansi.ON } else { Help.Ansi.AUTO } - - val results = cmd.parse(*args) - val app = cmd.getCommand() - if (cmd.isUsageHelpRequested) { - cmd.usage(System.out, defaultAnsiMode) - exitProcess(ExitCodes.SUCCESS) - } - if (cmd.isVersionHelpRequested) { - cmd.printVersionHelp(System.out, defaultAnsiMode) - exitProcess(ExitCodes.SUCCESS) - } - if (app.installShellExtensionsParser.installShellExtensions) { - System.out.println("Install shell extensions: ${app.installShellExtensionsParser.installShellExtensions}") - // ignore any parsing errors and run the program - exitProcess(app.call()) - } - val allErrors = results.flatMap { it.parseResult?.errors() ?: emptyList() } - if (allErrors.any()) { - val parameterExceptions = allErrors.asSequence().filter { it is ParameterException } - if (parameterExceptions.any()) { - System.err.println("${ShellConstants.RED}${parameterExceptions.map{ it.message }.joinToString()}${ShellConstants.RESET}") - parameterExceptions.filter { it is UnmatchedArgumentException}.forEach { (it as UnmatchedArgumentException).printSuggestions(System.out) } - usage(cmd, System.out, defaultAnsiMode) + val results = cmd.parseWithHandlers(RunLast().useOut(System.out).useAnsi(defaultAnsiMode), + DefaultExceptionHandler>().useErr(System.err).useAnsi(defaultAnsiMode), + *args) + // If an error code has been returned, use this and exit + results?.firstOrNull()?.let { + if (it is Int) { + exitProcess(it) + } else { exitProcess(ExitCodes.FAILURE) } - throw allErrors.first() } - exitProcess(app.call()) - } catch (e: Exception) { + // If no results returned, picocli ran something without invoking the main program, e.g. --help or --version, so exit successfully + exitProcess(ExitCodes.SUCCESS) + } catch (e: ExecutionException) { val throwable = e.cause ?: e if (this.verbose) { throwable.printStackTrace() } else { - System.err.println("${ShellConstants.RED}${throwable.rootMessage ?: "Use --verbose for more details"}${ShellConstants.RESET}") + System.err.println("*ERROR*: ${throwable.rootMessage ?: "Use --verbose for more details"}") } exitProcess(ExitCodes.FAILURE) } } -/** - * Simple base class for handling help, version, verbose and logging-level commands. - * As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node) - * Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations - */ @Command(mixinStandardHelpOptions = true, versionProvider = CordaVersionProvider::class, sortOptions = false, @@ -129,9 +115,9 @@ fun CordaCliWrapper.start(args: Array) { parameterListHeading = "%n@|bold,underline Parameters|@:%n%n", optionListHeading = "%n@|bold,underline Options|@:%n%n", commandListHeading = "%n@|bold,underline Commands|@:%n%n") -abstract class CordaCliWrapper(val alias: String, val description: String) : Callable { +abstract class CliWrapperBase(val alias: String, val description: String) : Callable { companion object { - private val logger by lazy { loggerFor() } + private val logger by lazy { contextLogger() } } // Raw args are provided for use in logging - this is a lateinit var rather than a constructor parameter as the class @@ -148,9 +134,6 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal ) var loggingLevel: Level = Level.INFO - @Mixin - lateinit var installShellExtensionsParser: InstallShellExtensionsParser - // This needs to be called before loggers (See: NodeStartup.kt:51 logger called by lazy, initLogging happens before). // Node's logging is more rich. In corda configurations two properties, defaultLoggingLevel and consoleLogLevel, are usually used. open fun initLogging() { @@ -169,7 +152,32 @@ abstract class CordaCliWrapper(val alias: String, val description: String) : Cal override fun call(): Int { initLogging() logger.info("Application Args: ${args.joinToString(" ")}") - installShellExtensionsParser.installOrUpdateShellExtensions(alias, this.javaClass.name) + return runProgram() + } +} + +/** + * Simple base class for handling help, version, verbose and logging-level commands. + * As versionProvider information from the MANIFEST file is used. It can be overwritten by custom version providers (see: Node) + * Picocli will prioritise versionProvider from the `@Command` annotation on the subclass, see: https://picocli.info/#_reuse_combinations + */ +abstract class CordaCliWrapper(alias: String, description: String) : CliWrapperBase(alias, description) { + companion object { + private val logger by lazy { contextLogger() } + } + + private val installShellExtensionsParser = InstallShellExtensionsParser(this) + + protected open fun additionalSubCommands(): Set = emptySet() + + fun subCommands(): Set { + return additionalSubCommands() + installShellExtensionsParser + } + + override fun call(): Int { + initLogging() + logger.info("Application Args: ${args.joinToString(" ")}") + installShellExtensionsParser.updateShellExtensions() return runProgram() } } diff --git a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt index cd271e1014..a9be0cbae0 100644 --- a/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt +++ b/tools/cliutils/src/main/kotlin/net/corda/cliutils/InstallShellExtensionsParser.kt @@ -2,13 +2,13 @@ package net.corda.cliutils import net.corda.core.internal.* import picocli.CommandLine +import picocli.CommandLine.Command import java.nio.file.Path import java.nio.file.Paths import java.nio.file.StandardCopyOption import java.util.* -import kotlin.system.exitProcess -private class ShellExtensionsGenerator(val alias: String, val className: String) { +private class ShellExtensionsGenerator(val parent: CordaCliWrapper) { private class SettingsFile(val filePath: Path) { private val lines: MutableList by lazy { getFileLines() } var fileModified: Boolean = false @@ -68,25 +68,27 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) private fun jarVersion(alias: String) = "# $alias - Version: ${CordaVersionProvider.releaseVersion}, Revision: ${CordaVersionProvider.revision}" private fun getAutoCompleteFileLocation(alias: String) = userHome / ".completion" / alias - private fun generateAutoCompleteFile(alias: String, className: String) { + private fun generateAutoCompleteFile(alias: String) { println("Generating $alias auto completion file") val autoCompleteFile = getAutoCompleteFileLocation(alias) autoCompleteFile.parent.createDirectories() - picocli.AutoComplete.main("-f", "-n", alias, className, "-o", autoCompleteFile.toStringWithDeWindowsfication()) + val hierarchy = CommandLine(parent) + parent.subCommands().forEach { hierarchy.addSubcommand(it.alias, it)} - // Append hash of file to autocomplete file - autoCompleteFile.toFile().appendText(jarVersion(alias)) + val builder = StringBuilder(picocli.AutoComplete.bash(alias, hierarchy)) + builder.append(jarVersion(alias)) + autoCompleteFile.writeText(builder.toString()) } fun installShellExtensions() { // Get jar location and generate alias command - val command = "alias $alias='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" - generateAutoCompleteFile(alias, className) + val command = "alias ${parent.alias}='java -jar \"${jarLocation.toStringWithDeWindowsfication()}\"'" + generateAutoCompleteFile(parent.alias) // Get bash settings file val bashSettingsFile = SettingsFile(userHome / ".bashrc") // Replace any existing alias. There can be only one. - bashSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + bashSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command) val completionFileCommand = "for bcfile in ~/.completion/* ; do . \$bcfile; done" bashSettingsFile.addIfNotExists(completionFileCommand) bashSettingsFile.updateAndBackupIfNecessary() @@ -95,17 +97,17 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) val zshSettingsFile = SettingsFile(userHome / ".zshrc") zshSettingsFile.addIfNotExists("autoload -U +X compinit && compinit") zshSettingsFile.addIfNotExists("autoload -U +X bashcompinit && bashcompinit") - zshSettingsFile.addOrReplaceIfStartsWith("alias $alias", command) + zshSettingsFile.addOrReplaceIfStartsWith("alias ${parent.alias}", command) zshSettingsFile.addIfNotExists(completionFileCommand) zshSettingsFile.updateAndBackupIfNecessary() - println("Installation complete, $alias is available in bash with autocompletion. ") - println("Type `$alias ` from the commandline.") + println("Installation complete, ${parent.alias} is available in bash with autocompletion. ") + println("Type `${parent.alias} ` from the commandline.") println("Restart bash for this to take effect, or run `. ~/.bashrc` in bash or `. ~/.zshrc` in zsh to re-initialise your shell now") } fun checkForAutoCompleteUpdate() { - val autoCompleteFile = getAutoCompleteFileLocation(alias) + val autoCompleteFile = getAutoCompleteFileLocation(parent.alias) // If no autocomplete file, it hasn't been installed, so don't do anything if (!autoCompleteFile.exists()) return @@ -113,25 +115,21 @@ private class ShellExtensionsGenerator(val alias: String, val className: String) var lastLine = "" autoCompleteFile.toFile().forEachLine { lastLine = it } - if (lastLine != jarVersion(alias)) { + if (lastLine != jarVersion(parent.alias)) { println("Old auto completion file detected... regenerating") - generateAutoCompleteFile(alias, className) + generateAutoCompleteFile(parent.alias) println("Restart bash for this to take effect, or run `. ~/.bashrc` to re-initialise bash now") } } } -class InstallShellExtensionsParser { - @CommandLine.Option(names = ["--install-shell-extensions"], description = ["Install alias and autocompletion for bash and zsh"]) - var installShellExtensions: Boolean = false - - fun installOrUpdateShellExtensions(alias: String, className: String) { - val generator = ShellExtensionsGenerator(alias, className) - if (installShellExtensions) { - generator.installShellExtensions() - exitProcess(0) - } else { - generator.checkForAutoCompleteUpdate() - } +@Command(helpCommand = true) +class InstallShellExtensionsParser(private val cliWrapper: CordaCliWrapper) : CliWrapperBase("install-shell-extensions", "Install alias and autocompletion for bash and zsh") { + private val generator = ShellExtensionsGenerator(cliWrapper) + override fun runProgram(): Int { + generator.installShellExtensions() + return ExitCodes.SUCCESS } + + fun updateShellExtensions() = generator.checkForAutoCompleteUpdate() } \ No newline at end of file diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt index e3b51b8e85..92af11ef76 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/notaries/NotaryCopier.kt @@ -25,7 +25,7 @@ class NotaryCopier(val cacheDir: File) : NodeCopier(cacheDir) { fun generateNodeInfo(dirToGenerateFrom: File): File { val nodeInfoGeneratorProcess = ProcessBuilder() - .command(listOf("java", "-jar", "corda.jar", "--just-generate-node-info")) + .command(listOf("java", "-jar", "corda.jar", "generate-node-info")) .directory(dirToGenerateFrom) .inheritIO() .start() diff --git a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml index ce5ec0bab1..20a9fde8fb 100644 --- a/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml +++ b/tools/shell-cli/src/test/resources/net.corda.tools.shell.StandaloneShell.yml @@ -21,11 +21,6 @@ required: false multiParam: false acceptableValues: [] - - parameterName: "--install-shell-extensions" - parameterType: "boolean" - required: false - multiParam: false - acceptableValues: [] - parameterName: "--log-to-console" parameterType: "boolean" required: false From 16453ebfcc15ba4013216f6f4ad80bf8426a4d72 Mon Sep 17 00:00:00 2001 From: bpaunescu Date: Wed, 24 Oct 2018 14:36:01 +0100 Subject: [PATCH 9/9] update OWASP to latest version after kotlin update (#4109) --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 41f3f98e69..7cfd603ad1 100644 --- a/build.gradle +++ b/build.gradle @@ -49,7 +49,7 @@ buildscript { ext.rxjava_version = '1.3.8' ext.dokka_version = '0.9.17' ext.eddsa_version = '0.2.0' - ext.dependency_checker_version = '3.1.0' + ext.dependency_checker_version = '3.3.2' ext.commons_collections_version = '4.1' ext.beanutils_version = '1.9.3' ext.crash_version = 'cadb53544fbb3c0fb901445da614998a6a419488'