diff --git a/README.md b/README.md index 1601315cc8..eba677a651 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) +

+ Corda +

diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index db445caa55..c5cceeb1b1 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -1,20 +1,23 @@ package net.corda.client.rpc +import com.github.benmanes.caffeine.cache.Caffeine import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme import net.corda.core.context.Actor import net.corda.core.context.Trace import net.corda.core.identity.CordaX500Name +import net.corda.core.internal.PLATFORM_VERSION +import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.messaging.CordaRPCOps +import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.internal.effectiveSerializationEnv import net.corda.core.utilities.NetworkHostAndPort -import net.corda.core.messaging.ClientRpcSslOptions import net.corda.core.utilities.days import net.corda.core.utilities.minutes import net.corda.core.utilities.seconds import net.corda.nodeapi.internal.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport -import net.corda.core.internal.PLATFORM_VERSION import net.corda.serialization.internal.AMQP_RPC_CLIENT_CONTEXT +import net.corda.serialization.internal.amqp.SerializerFactory import java.time.Duration /** @@ -293,7 +296,7 @@ class CordaRPCClient private constructor( effectiveSerializationEnv } catch (e: IllegalStateException) { try { - AMQPClientSerializationScheme.initialiseSerialization(classLoader) + AMQPClientSerializationScheme.initialiseSerialization(classLoader, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap()) } catch (e: IllegalStateException) { // Race e.g. two of these constructed in parallel, ignore. } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt index a0e2bfc307..3ef215f9e8 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/serialization/amqp/AMQPClientSerializationScheme.kt @@ -3,7 +3,7 @@ package net.corda.client.rpc.internal.serialization.amqp import net.corda.core.cordapp.Cordapp import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.SerializationContext -import net.corda.core.serialization.SerializationContext.* +import net.corda.core.serialization.SerializationContext.UseCase import net.corda.core.serialization.SerializationCustomSerializer import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv @@ -19,24 +19,25 @@ import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer * This scheme is for use by the RPC Client calls. */ class AMQPClientSerializationScheme( - cordappCustomSerializers: Set>, - serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> + cordappCustomSerializers: Set>, + serializerFactoriesForContexts: MutableMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) + constructor(cordapps: List, serializerFactoriesForContexts: MutableMap, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts) @Suppress("UNUSED") constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) companion object { /** Call from main only. */ - fun initialiseSerialization(classLoader: ClassLoader? = null) { - nodeSerializationEnv = createSerializationEnv(classLoader) + fun initialiseSerialization(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap, SerializerFactory> = AccessOrderLinkedHashMap { 128 }) { + nodeSerializationEnv = createSerializationEnv(classLoader, serializerFactoriesForContexts) } - fun createSerializationEnv(classLoader: ClassLoader? = null): SerializationEnvironment { + fun createSerializationEnv(classLoader: ClassLoader? = null, serializerFactoriesForContexts: MutableMap, SerializerFactory> = AccessOrderLinkedHashMap { 128 }): SerializationEnvironment { return SerializationEnvironment.with( SerializationFactoryImpl().apply { - registerScheme(AMQPClientSerializationScheme(emptyList())) + registerScheme(AMQPClientSerializationScheme(emptyList(), serializerFactoriesForContexts)) }, storageContext = AMQP_STORAGE_CONTEXT, p2pContext = if (classLoader != null) AMQP_P2P_CONTEXT.withClassLoader(classLoader) else AMQP_P2P_CONTEXT, diff --git a/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt b/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt index 57320d7eb9..294aebd427 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/ContractsDSLTests.kt @@ -13,170 +13,166 @@ import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue -class ContractsDSLTests { - class UnwantedCommand : CommandData +class UnwantedCommand : CommandData - interface TestCommands : CommandData { - class CommandOne : TypeOnlyCommandData(), TestCommands - class CommandTwo : TypeOnlyCommandData(), TestCommands +interface TestCommands : CommandData { + class CommandOne : TypeOnlyCommandData(), TestCommands + class CommandTwo : TypeOnlyCommandData(), TestCommands +} + +val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) +val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) + +val validCommandOne = CommandWithParties(listOf(megaCorp.publicKey, miniCorp.publicKey), listOf(megaCorp.party, miniCorp.party), TestCommands.CommandOne()) +val validCommandTwo = CommandWithParties(listOf(megaCorp.publicKey), listOf(megaCorp.party), TestCommands.CommandTwo()) +val invalidCommand = CommandWithParties(emptyList(), emptyList(), UnwantedCommand()) + +@RunWith(Parameterized::class) +class RequireSingleCommandTests(private val testFunction: (Collection>) -> CommandWithParties, + @Suppress("UNUSED_PARAMETER") description: String) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection> -> commands.requireSingleCommand() }, "Inline version"), + arrayOf({ commands: Collection> -> commands.requireSingleCommand(TestCommands::class.java) }, "Interop version") + ) } - private companion object { - val megaCorp = TestIdentity(CordaX500Name("MegaCorp", "London", "GB")) - val miniCorp = TestIdentity(CordaX500Name("MiniCorp", "London", "GB")) - - val validCommandOne = CommandWithParties(listOf(megaCorp.publicKey, miniCorp.publicKey), listOf(megaCorp.party, miniCorp.party), TestCommands.CommandOne()) - val validCommandTwo = CommandWithParties(listOf(megaCorp.publicKey), listOf(megaCorp.party), TestCommands.CommandTwo()) - val invalidCommand = CommandWithParties(emptyList(), emptyList(), UnwantedCommand()) + @Test + fun `check function returns one value`() { + val commands = listOf(validCommandOne, invalidCommand) + val returnedCommand = testFunction(commands) + assertEquals(returnedCommand, validCommandOne, "they should be the same") } - @RunWith(Parameterized::class) - class RequireSingleCommandTests(private val testFunction: (Collection>) -> CommandWithParties, + @Test(expected = IllegalArgumentException::class) + fun `check error is thrown if more than one valid command`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands) + } + + @Test + fun `check error is thrown when command is of wrong type`() { + val commands = listOf(invalidCommand) + Assertions.assertThatThrownBy { testFunction(commands) } + .isInstanceOf(IllegalStateException::class.java) + .hasMessage("Required net.corda.core.contracts.TestCommands command") + } +} + +@RunWith(Parameterized::class) +class SelectWithSingleInputsTests(private val testFunction: (Collection>, PublicKey?, AbstractParty?) -> Iterable>, + @Suppress("UNUSED_PARAMETER") description: String) { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(signer, party) }, "Inline version"), + arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(TestCommands::class.java, signer, party) }, "Interop version") + ) + } + + @Test + fun `check that function returns all values`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands, null, null) + assertEquals(2, commands.size) + assertTrue(commands.contains(validCommandOne)) + assertTrue(commands.contains(validCommandTwo)) + } + + @Test + fun `check that function does not return invalid command types`() { + val commands = listOf(validCommandOne, invalidCommand) + val filteredCommands = testFunction(commands, null, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(invalidCommand)) + } + + @Test + fun `check that function returns commands from valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, miniCorp.publicKey, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, miniCorp.party).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } +} + +@RunWith(Parameterized::class) +class SelectWithMultipleInputsTests(private val testFunction: (Collection>, Collection?, Collection?) -> Iterable>, @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection> -> commands.requireSingleCommand() }, "Inline version"), - arrayOf({ commands: Collection> -> commands.requireSingleCommand(TestCommands::class.java) }, "Interop version") - ) - } - - @Test - fun `check function returns one value`() { - val commands = listOf(validCommandOne, invalidCommand) - val returnedCommand = testFunction(commands) - assertEquals(returnedCommand, validCommandOne, "they should be the same") - } - - @Test(expected = IllegalArgumentException::class) - fun `check error is thrown if more than one valid command`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands) - } - - @Test - fun `check error is thrown when command is of wrong type`() { - val commands = listOf(invalidCommand) - Assertions.assertThatThrownBy { testFunction(commands) } - .isInstanceOf(IllegalStateException::class.java) - .hasMessage("Required net.corda.core.contracts.ContractsDSLTests.TestCommands command") - } + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{1}") + fun data(): Collection> = listOf( + arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(signers, party) }, "Inline version"), + arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(TestCommands::class.java, signers, party) }, "Interop version") + ) } - @RunWith(Parameterized::class) - class SelectWithSingleInputsTests(private val testFunction: (Collection>, PublicKey?, AbstractParty?) -> Iterable>, - @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(signer, party) }, "Inline version"), - arrayOf({ commands: Collection>, signer: PublicKey?, party: AbstractParty? -> commands.select(TestCommands::class.java, signer, party) }, "Interop version") - ) - } - - @Test - fun `check that function returns all values`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands, null, null) - assertEquals(2, commands.size) - assertTrue(commands.contains(validCommandOne)) - assertTrue(commands.contains(validCommandTwo)) - } - - @Test - fun `check that function does not return invalid command types`() { - val commands = listOf(validCommandOne, invalidCommand) - val filteredCommands = testFunction(commands, null, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(invalidCommand)) - } - - @Test - fun `check that function returns commands from valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, miniCorp.publicKey, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, miniCorp.party).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } + @Test + fun `check that function returns all values`() { + val commands = listOf(validCommandOne, validCommandTwo) + testFunction(commands, null, null) + assertEquals(2, commands.size) + assertTrue(commands.contains(validCommandOne)) + assertTrue(commands.contains(validCommandTwo)) } - @RunWith(Parameterized::class) - class SelectWithMultipleInputsTests(private val testFunction: (Collection>, Collection?, Collection?) -> Iterable>, - @Suppress("UNUSED_PARAMETER") description: String) { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{1}") - fun data(): Collection> = listOf( - arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(signers, party) }, "Inline version"), - arrayOf({ commands: Collection>, signers: Collection?, party: Collection? -> commands.select(TestCommands::class.java, signers, party) }, "Interop version") - ) - } - - @Test - fun `check that function returns all values`() { - val commands = listOf(validCommandOne, validCommandTwo) - testFunction(commands, null, null) - assertEquals(2, commands.size) - assertTrue(commands.contains(validCommandOne)) - assertTrue(commands.contains(validCommandTwo)) - } - - @Test - fun `check that function does not return invalid command types`() { - val commands = listOf(validCommandOne, invalidCommand) - val filteredCommands = testFunction(commands, null, null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(invalidCommand)) - } - - @Test - fun `check that function returns commands from valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, listOf(megaCorp.publicKey), null).toList() - assertEquals(2, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertTrue(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from all valid signers`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, listOf(miniCorp.publicKey, megaCorp.publicKey), null).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, listOf(megaCorp.party)).toList() - assertEquals(2, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertTrue(filteredCommands.contains(validCommandTwo)) - } - - @Test - fun `check that function returns commands from all valid parties`() { - val commands = listOf(validCommandOne, validCommandTwo) - val filteredCommands = testFunction(commands, null, listOf(miniCorp.party, megaCorp.party)).toList() - assertEquals(1, filteredCommands.size) - assertTrue(filteredCommands.contains(validCommandOne)) - assertFalse(filteredCommands.contains(validCommandTwo)) - } + @Test + fun `check that function does not return invalid command types`() { + val commands = listOf(validCommandOne, invalidCommand) + val filteredCommands = testFunction(commands, null, null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(invalidCommand)) } -} \ No newline at end of file + + @Test + fun `check that function returns commands from valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, listOf(megaCorp.publicKey), null).toList() + assertEquals(2, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertTrue(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from all valid signers`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, listOf(miniCorp.publicKey, megaCorp.publicKey), null).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, listOf(megaCorp.party)).toList() + assertEquals(2, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertTrue(filteredCommands.contains(validCommandTwo)) + } + + @Test + fun `check that function returns commands from all valid parties`() { + val commands = listOf(validCommandOne, validCommandTwo) + val filteredCommands = testFunction(commands, null, listOf(miniCorp.party, megaCorp.party)).toList() + assertEquals(1, filteredCommands.size) + assertTrue(filteredCommands.contains(validCommandOne)) + assertFalse(filteredCommands.contains(validCommandTwo)) + } +} diff --git a/docs/source/api-contract-constraints.rst b/docs/source/api-contract-constraints.rst index 71b3c6a19d..8d3a953086 100644 --- a/docs/source/api-contract-constraints.rst +++ b/docs/source/api-contract-constraints.rst @@ -94,6 +94,8 @@ time effectively stop being a part of the network. **Signature constraints.** These are not yet supported, but once implemented they will allow a state to require a JAR signed by a specified identity, via the regular Java ``jarsigner`` tool. This will be the most flexible type and the smoothest to deploy: no restarts or contract upgrade transactions are needed. +When CorDapp is build using :ref:`corda-gradle-plugin ` the JAR is signed +by Corda development key by default, an external keystore can be configured or signing can be disabled. **Defaults.** Currently, the default constraint type is either a zone constraint, if the network parameters in effect when the transaction is built contain an entry for that contract class, or a hash constraint if not. Once the Signature Constraints are introduced, diff --git a/docs/source/api-persistence.rst b/docs/source/api-persistence.rst index 950378d953..fe85cd0321 100644 --- a/docs/source/api-persistence.rst +++ b/docs/source/api-persistence.rst @@ -113,7 +113,7 @@ Several examples of entities and mappings are provided in the codebase, includin :language: kotlin .. note:: Ensure table and column names are compatible with the naming convention of database vendors for which the Cordapp will be deployed, - e.g. prior to Oracle 12.2 the maximum length of table/column name is 30 bytes (the exact number of characters depends on the database encoding). + e.g. for Oracle database, prior to version 12.2 the maximum length of table/column name is 30 bytes (the exact number of characters depends on the database encoding). Identity mapping ---------------- diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 7390f69a55..dbcee97aeb 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -11,20 +11,23 @@ Interacting with a node Overview -------- -You should interact with your node using the `CordaRPCClient`_ library. This library that allows you to easily -write clients in a JVM-compatible language to interact with a running node. The library connects to the node using a -message queue protocol and then provides a simple RPC interface to interact with the node. You make calls on a JVM -object as normal, and the marshalling back and forth is handled for you. +To interact with your node, you need to write a client in a JVM-compatible language using the `CordaRPCClient`_ class. +This class allows you to connect to your node via a message queue protocol and provides a simple RPC interface for +interacting with the node. You make calls on a JVM object as normal, and the marshalling back-and-forth is handled for +you. .. warning:: The built-in Corda webserver is deprecated and unsuitable for production use. If you want to interact with - your node via HTTP, you will need to stand up your own webserver, then create an RPC connection between your node - and this webserver using the `CordaRPCClient`_ library. You can find an example of how to do this using the popular - Spring Boot server `here `_. + your node via HTTP, you will need to stand up your own webserver that connects to your node using the + `CordaRPCClient`_ class. You can find an example of how to do this using the popular Spring Boot server + `here `_. Connecting to a node via RPC ---------------------------- -`CordaRPCClient`_ provides a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_. -`CordaRPCConnection`_ provides a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_ +To use `CordaRPCClient`_, you must add ``net.corda:corda-rpc:$corda_release_version`` as a ``cordaCompile`` dependency +in your client's ``build.gradle`` file. + +`CordaRPCClient`_ has a ``start`` method that takes the node's RPC address and returns a `CordaRPCConnection`_. +`CordaRPCConnection`_ has a ``proxy`` method that takes an RPC username and password and returns a `CordaRPCOps`_ object that you can use to interact with the node. Here is an example of using `CordaRPCClient`_ to connect to a node and log the current time on its internal clock: diff --git a/docs/source/cordapp-build-systems.rst b/docs/source/cordapp-build-systems.rst index cd54fd9768..8c39aa27be 100644 --- a/docs/source/cordapp-build-systems.rst +++ b/docs/source/cordapp-build-systems.rst @@ -109,6 +109,108 @@ in Gradle. See the example below, specifically the ``apache-commons`` include. For further information about managing dependencies, see `the Gradle docs `_. +.. _cordapp_build_system_signing_cordapp_jar_ref: + +Signing the CorDapp JAR +^^^^^^^^^^^^^^^^^^^^^^^ +The ``cordapp`` plugin can sign the generated CorDapp JAR file using `JAR signing and verification tool `_. +Signing the CorDapp enables its contract classes to use signature constraints instead of other types of the constraints, +for constraints explanation refer to :doc:`api-contract-constraints`. +By default the JAR file is signed by Corda development certificate. +The signing process can be disabled or configured to use an external keystore. +The ``signing`` entry may contain the following parameters: + + * ``enabled`` the control flag to enable signing process, by default is set to ``true``, set to ``false`` to disable signing + * ``options`` any relevant parameters of `SignJar ANT task `_, + by default the JAR file is signed with Corda development key, the external keystore can be specified, + the minimal list of required options is shown below, for other options referer to `SignJar task `_: + + * ``keystore`` the path to the keystore file, by default *cordadevcakeys.jks* keystore is shipped with the plugin + * ``alias`` the alias to sign under, the default value is *cordaintermediateca* + * ``storepass`` the keystore password, the default value is *cordacadevpass* + * ``keypass`` the private key password if it's different than the password for the keystore, the default value is *cordacadevkeypass* + * ``storetype`` the keystore type, the default value is *JKS* + +The parameters can be also set by system properties passed to Gradle build process. +The system properties should be named as the relevant option name prefixed with '*signing.*', e.g. +a value for ``alias`` can be taken from the ``signing.alias`` system property. The following system properties can be used: +``signing.enabled``, ``signing.keystore``, ``signing.alias``, ``signing.storepass``, ``signing.keypass``, ``signing.storetype``. +The resolution order of a configuration value is as follows: the signing process takes a value specified in the ``signing`` entry first, +the empty string *""* is also considered as the correct value. +If the option is not set, the relevant system property named *signing.option* is tried. +If the system property is not set then the value defaults to the configuration of the Corda development certificate. + +The example ``cordapp`` plugin with plugin ``signing`` configuration: + +.. sourcecode:: groovy + + cordapp { + signing { + enabled true + options { + keystore "/path/to/jarSignKeystore.p12" + alias "cordapp-signer" + storepass "secret1!" + keypass "secret1!" + storetype "PKCS12" + } + } + //... + +CorDapp auto-signing allows to use signature constraints for contracts from the CorDapp +without need to create a keystore and configure the ``cordapp`` plugin. +For production deployment ensure to sign the CorDapp using your own certificate e.g. by setting system properties to point to an external keystore +or by disabling signing in ``cordapp`` plugin and signing the CordDapp JAR downstream in your build pipeline. +CorDapp signed by Corda development certificate is accepted by Corda node only when running in the development mode. + +Signing options can be contextually overwritten by the relevant system properties as described above. +This allows the single ``build.gradle`` file to be used for a development build (defaulting to the Corda development keystore) +and for a production build (using an external keystore). +The example system properties setup for the build process which overrides signing options: + +.. sourcecode:: shell + + ./gradlew -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" + +Without providing the system properties, the build will sign the CorDapp with the default Corda development keystore: + +.. sourcecode:: shell + + ./gradlew + +CorDapp signing can be disabled for a build: + +.. sourcecode:: shell + + ./gradlew -Dsigning.enabled=false + +Other system properties can be explicitly assigned to options by calling ``System.getProperty`` in ``cordapp`` plugin configuration. +For example the below configuration sets the specific signing algorithm when a system property is available otherwise defaults to an empty string: + +.. sourcecode:: groovy + + cordapp { + signing { + options { + sigalg System.getProperty('custom.sigalg','') + } + } + //... + +Then the build process can set the value for *custom.sigalg* system property and other system properties recognized by ``cordapp`` plugin: + +.. sourcecode:: shell + + ./gradlew -Dcustom.sigalg="SHA256withECDSA" -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" + +To check if CorDapp is signed use `JAR signing and verification tool `_: + +.. sourcecode:: shell + + jarsigner --verify path/to/cordapp.jar + +Cordformation plugin can also sign CorDapps JARs, when deploying set of nodes, see :doc:`generating-a-node`. + Example ^^^^^^^ Below is a sample of what a CorDapp's Gradle dependencies block might look like. When building your own CorDapp, you should diff --git a/docs/source/generating-a-node.rst b/docs/source/generating-a-node.rst index e4d33a91e7..76e424ee36 100644 --- a/docs/source/generating-a-node.rst +++ b/docs/source/generating-a-node.rst @@ -143,6 +143,66 @@ To copy the same file to all nodes `ext.drivers` can be defined in the top level } } +Signing Cordapp JARs +^^^^^^^^^^^^^^^^^^^^ +Cordform entry ``signing`` configures the signing of CorDapp JARs. +Signing the CorDapp enables its contract classes to use signature constraints instead of other types of the constraints :doc:`api-contract-constraints`. +By default all CorDapp JARs are signed by Corda development certificate. +The sign task may use an external keystore, or create a new one. +The ``signing`` entry may contain the following parameters: + +* ``enabled`` the control flag to enable signing process, by default is set to ``true``, set to ``false`` to disable signing +* ``all`` if set to ``true`` (by default) all CorDapps inside *cordapp* subdirectory will be signed, otherwise if ``false`` then only the generated Cordapp will be signed +* ``options`` any relevant parameters of `SignJar ANT task `_ and `GenKey ANT task `_, + by default the JAR file is signed by Corda development key, the external keystore can be specified, + the minimal list of required options is shown below, for other options referer to `SignJar task `_: + + * ``keystore`` the path to the keystore file, by default *cordadevcakeys.jks* keystore is shipped with the plugin + * ``alias`` the alias to sign under, the default value is *cordaintermediateca* + * ``storepass`` the keystore password, the default value is *cordacadevpass* + * ``keypass`` the private key password if it's different than the password for the keystore, the default value is *cordacadevkeypass* + * ``storetype`` the keystore type, the default value is *JKS* + * ``dname`` the distinguished name for entity, the option is used when ``generateKeystore true`` only + * ``keyalg`` the method to use when generating name-value pair, the value defaults to *RSA* as Corda doesn't support *DSA*, the option is used when ``generateKeystore true`` only + +* ``generateKeystore`` the flag to generate a keystore, it is set to ``false`` by default. If set to ``true`` then ad hock keystore is created and its key isused + instead of the default Corda development key or any external key. + The same ``options`` to specify an external keystore are used to define the newly created keystore. Additionally + ``dname`` and ``keyalg`` are required. Other options are described in `GenKey task `_. + If the existing keystore is already present the task will reuse it, however if the file is inside the *build* directory, + then it will be deleted when Gradle *clean* task is run. + +The example below shows the minimal set of ``options`` needed to create a dummy keystore: + +.. sourcecode:: groovy + + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + signing { + enabled true + generateKeystore true + all false + options { + keystore "./build/nodes/jarSignKeystore.p12" + alias "cordapp-signer" + storepass "secret1!" + storetype "PKCS12" + dname "OU=Dummy Cordapp Distributor, O=Corda, L=London, C=GB" + keyalg "RSA" + } + } + //... + +Contracts classes from signed CorDapp JARs will be checked by signature constraints by default. +You can force them to be checked by zone constraints by adding contract class names to ``includeWhitelist`` entry, +the list will generate *include_whitelist.txt* file used internally by :doc:`network-bootstrapper` tool. +Refer to :doc:`api-contract-constraints` to understand implication of different constraint types before adding ``includeWhitelist`` to ``deployNodes`` task. +The snippet below configures contracts classes from Finance CorDapp to be verified using zone constraints instead of signature constraints: + +.. sourcecode:: groovy + + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { + includeWhitelist = [ "net.corda.finance.contracts.asset.Cash", "net.corda.finance.contracts.asset.CommercialPaper" ] + //... Specifying a custom webserver ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/node-database.rst b/docs/source/node-database.rst index 310b718ad2..497a81ed08 100644 --- a/docs/source/node-database.rst +++ b/docs/source/node-database.rst @@ -324,7 +324,7 @@ Example node configuration for PostgreSQL: dataSourceProperties = { dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource" - dataSource.url = "jdbc:postgresql://[HOST]:[PORT]/postgres" + dataSource.url = "jdbc:postgresql://[HOST]:[PORT]/[DATABASE]" dataSource.user = [USER] dataSource.password = [PASSWORD] } @@ -353,6 +353,60 @@ To delete existing data from the database, drop the existing schema and recreate DROP SCHEMA IF EXISTS "[SCHEMA]" CASCADE; +Node database tables +^^^^^^^^^^^^^^^^^^^^ + +By default, the node database has the following tables: + ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Table name | Columns | ++=============================+==========================================================================================================================================================================================================+ +| DATABASECHANGELOG | ID, AUTHOR, FILENAME, DATEEXECUTED, ORDEREXECUTED, EXECTYPE, MD5SUM, DESCRIPTION, COMMENTS, TAG, LIQUIBASE, CONTEXTS, LABELS, DEPLOYMENT_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| DATABASECHANGELOGLOCK | ID, LOCKED, LOCKGRANTED, LOCKEDBY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_ATTACHMENTS | ATT_ID, CONTENT, FILENAME, INSERTION_DATE, UPLOADER | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_ATTACHMENTS_CONTRACTS | ATT_ID, CONTRACT_CLASS_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_CHECKPOINTS | CHECKPOINT_ID, CHECKPOINT_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_CONTRACT_UPGRADES | STATE_REF, CONTRACT_CLASS_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_IDENTITIES | PK_HASH, IDENTITY_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFOS | NODE_INFO_ID, NODE_INFO_HASH, PLATFORM_VERSION, SERIAL | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFO_HOSTS | HOST_NAME, PORT, NODE_INFO_ID, HOSTS_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_INFO_PARTY_CERT | PARTY_NAME, ISMAIN, OWNING_KEY_HASH, PARTY_CERT_BINARY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_LINK_NODEINFO_PARTY | NODE_INFO_ID, PARTY_NAME | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_MESSAGE_IDS | MESSAGE_ID, INSERTION_TIME, SENDER, SEQUENCE_NUMBER | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_NAMES_IDENTITIES | NAME, PK_HASH | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_OUR_KEY_PAIRS | PUBLIC_KEY_HASH, PRIVATE_KEY, PUBLIC_KEY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_PROPERTIES | PROPERTY_KEY, PROPERTY_VALUE | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_SCHEDULED_STATES | OUTPUT_INDEXTRANSACTION_IDSCHEDULED_AT | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| NODE_TRANSACTIONS | TX_ID, TRANSACTION_VALUE, STATE_MACHINE_RUN_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_FUNGIBLE_STATES | OUTPUT_INDEX, TRANSACTION_ID, ISSUER_NAME, ISSUER_REF, OWNER_NAME, QUANTITY | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_FUNGIBLE_STATES_PARTS | OUTPUT_INDEX, TRANSACTION_ID, PARTICIPANTS | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_LINEAR_STATES | OUTPUT_INDEX, TRANSACTION_ID, EXTERNAL_ID, UUID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_LINEAR_STATES_PARTS | OUTPUT_INDEX, TRANSACTION_ID, PARTICIPANTS | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_STATES | OUTPUT_INDEX, TRANSACTION_ID, CONSUMED_TIMESTAMP, CONTRACT_STATE_CLASS_NAME, LOCK_ID, LOCK_TIMESTAMP, NOTARY_NAME, RECORDED_TIMESTAMP, STATE_STATUS, RELEVANCY_STATUS, CONSTRAINT_TYPE, CONSTRAINT_DATA | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| VAULT_TRANSACTION_NOTES | SEQ_NO, NOTE, TRANSACTION_ID | ++-----------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ Guideline for adding support for other databases ```````````````````````````````````````````````` diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 878179f985..b91d653b4b 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -39,22 +39,23 @@ weakly or untyped string-based serialisation schemes like JSON or XML. The prima Whitelisting ------------ -In classic Java serialization, any class on the JVM classpath can be deserialized. This has shown to be a source of exploits -and vulnerabilities by exploiting the large set of 3rd party libraries on the classpath as part of the dependencies of -a JVM application and a carefully crafted stream of bytes to be deserialized. In Corda, we prevent just any class from -being deserialized (and pro-actively during serialization) by insisting that each object's class belongs on a whitelist -of allowed classes. +In classic Java serialization, any class on the JVM classpath can be deserialized. This is a source of exploits and +vulnerabilities that exploit the large set of third-party libraries that are added to the classpath as part of a JVM +application's dependencies and carefully craft a malicious stream of bytes to be deserialized. In Corda, we strictly +control which classes can be deserialized (and, pro-actively, serialized) by insisting that each (de)serializable class +is part of a whitelist of allowed classes. -Classes get onto the whitelist via one of three mechanisms: +To add a class to the whitelist, you must use either of the following mechanisms: -#. Via the ``@CordaSerializable`` annotation. In order to whitelist a class, this annotation can be present on the - class itself, on any of the super classes or on any interface implemented by the class or super classes or any - interface extended by an interface implemented by the class or superclasses. -#. By implementing the ``SerializationWhitelist`` interface and specifying a list of `whitelist` classes. -#. Via the built in Corda whitelist (see the class ``DefaultWhitelist``). Whilst this is not user editable, it does list - common JDK classes that have been whitelisted for your convenience. +#. Add the ``@CordaSerializable`` annotation to the class. This annotation can be present on the + class itself, on any super class of the class, on any interface implemented by the class or its super classes, or any + interface extended by an interface implemented by the class or its super classes. +#. Implement the ``SerializationWhitelist`` interface and specify a list of whitelisted classes. -The annotation is the preferred method for whitelisting. An example is shown in :doc:`tutorial-clientrpc-api`. +There is also a built-in Corda whitelist (see the ``DefaultWhitelist`` class) that whitelists common JDK classes for +convenience. This whitelist is not user-editable. + +The annotation is the preferred method for whitelisting. An example is shown in :doc:`tutorial-clientrpc-api`. It's reproduced here as an example of both ways you can do this for a couple of example classes. .. literalinclude:: example-code/src/main/kotlin/net/corda/docs/kotlin/ClientRpcTutorial.kt diff --git a/finance/build.gradle b/finance/build.gradle index 3f764da541..e3ceca1403 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -90,6 +90,8 @@ cordapp { targetPlatformVersion corda_platform_version.toInteger() minimumPlatformVersion 1 } + // By default the Cordapp is signed by Corda development certificate, for production build pass the following system properties to Gradle to use specific keystore e.g: + // ./gradlew -Dsigning.enabled="true" -Dsigning.keystore="/path/to/keystore.jks" -Dsigning.alias="alias" -Dsigning.storepass="password" -Dsigning.keypass="password" } publish { 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 09586fc399..5da15dc78e 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -3,6 +3,7 @@ package net.corda.node.internal import com.codahale.metrics.JmxReporter import com.codahale.metrics.MetricFilter import com.codahale.metrics.MetricRegistry +import com.github.benmanes.caffeine.cache.Caffeine import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter import com.palominolabs.metrics.newrelic.NewRelicReporter import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme @@ -22,6 +23,7 @@ import net.corda.core.messaging.RPCOps import net.corda.core.node.NetworkParameters import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub +import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.internal.SerializationEnvironment import net.corda.core.serialization.internal.nodeSerializationEnv import net.corda.core.utilities.NetworkHostAndPort @@ -56,6 +58,7 @@ import net.corda.nodeapi.internal.config.User import net.corda.nodeapi.internal.crypto.X509Utilities import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException import net.corda.serialization.internal.* +import net.corda.serialization.internal.amqp.SerializerFactory import org.apache.commons.lang.SystemUtils import org.h2.jdbc.JdbcSQLException import org.slf4j.Logger @@ -502,8 +505,8 @@ open class Node(configuration: NodeConfiguration, val classloader = cordappLoader.appClassLoader nodeSerializationEnv = SerializationEnvironment.with( SerializationFactoryImpl().apply { - registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps)) - registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps)) + registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap())) + registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps, Caffeine.newBuilder().maximumSize(128).build, SerializerFactory>().asMap())) }, p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader), rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader), diff --git a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt index e1183be6b7..dfe6b6fafd 100644 --- a/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt +++ b/node/src/main/kotlin/net/corda/node/serialization/amqp/AMQPServerSerializationScheme.kt @@ -9,7 +9,6 @@ import net.corda.serialization.internal.amqp.AbstractAMQPSerializationScheme import net.corda.serialization.internal.amqp.AccessOrderLinkedHashMap import net.corda.serialization.internal.amqp.SerializerFactory import net.corda.serialization.internal.amqp.custom.RxNotificationSerializer -import java.util.concurrent.ConcurrentHashMap /** * When set as the serialization scheme, defines the RPC Server serialization scheme as using the Corda @@ -17,9 +16,10 @@ import java.util.concurrent.ConcurrentHashMap */ class AMQPServerSerializationScheme( cordappCustomSerializers: Set>, - serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory> + serializerFactoriesForContexts: MutableMap, SerializerFactory> ) : AbstractAMQPSerializationScheme(cordappCustomSerializers, serializerFactoriesForContexts) { constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap { 128 }) + constructor(cordapps: List, serializerFactoriesForContexts: MutableMap, SerializerFactory>) : this(cordapps.customSerializers, serializerFactoriesForContexts) constructor() : this(emptySet(), AccessOrderLinkedHashMap { 128 }) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt index 0ca5cd2ffb..0d9a24c898 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/AttachmentStorageInternal.kt @@ -2,6 +2,7 @@ package net.corda.node.services.persistence import net.corda.core.node.services.AttachmentId import net.corda.core.node.services.AttachmentStorage +import net.corda.nodeapi.exceptions.DuplicateAttachmentException import java.io.InputStream interface AttachmentStorageInternal : AttachmentStorage { @@ -10,4 +11,9 @@ interface AttachmentStorageInternal : AttachmentStorage { * and is only for the node. */ fun privilegedImportAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId + + /** + * Similar to above but returns existing [AttachmentId] instead of throwing [DuplicateAttachmentException] + */ + fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId } diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index 1ea12ffb15..37f5a38d6c 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -6,13 +6,11 @@ import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream -import net.corda.core.ClientRelevantError import net.corda.core.CordaRuntimeException import net.corda.core.contracts.Attachment import net.corda.core.contracts.ContractAttachment import net.corda.core.contracts.ContractClassName import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.sha256 import net.corda.core.internal.* import net.corda.core.node.ServicesForResolution @@ -286,6 +284,14 @@ class NodeAttachmentService( return import(jar, uploader, filename) } + override fun privilegedImportOrGetAttachment(jar: InputStream, uploader: String, filename: String?): AttachmentId { + return try { + import(jar, uploader, filename) + } catch (faee: java.nio.file.FileAlreadyExistsException) { + AttachmentId.parse(faee.message!!) + } + } + override fun hasAttachment(attachmentId: AttachmentId): Boolean = database.transaction { currentDBSession().find(NodeAttachmentService.DBAttachment::class.java, attachmentId.toString()) != null } @@ -306,9 +312,7 @@ class NodeAttachmentService( val id = bytes.sha256() if (!hasAttachment(id)) { checkIsAValidJAR(bytes.inputStream()) - val jarSigners = getSigners(bytes) - val session = currentDBSession() val attachment = NodeAttachmentService.DBAttachment( attId = id.toString(), @@ -318,14 +322,24 @@ class NodeAttachmentService( contractClassNames = contractClassNames, signers = jarSigners ) - session.save(attachment) attachmentCount.inc() log.info("Stored new attachment $id") - id - } else { - throw DuplicateAttachmentException(id.toString()) + return@withContractsInJar id } + if (isUploaderTrusted(uploader)) { + val session = currentDBSession() + val attachment = session.get(NodeAttachmentService.DBAttachment::class.java, id.toString()) + // update the `upLoader` field (as the existing attachment may have been resolved from a peer) + if (attachment.uploader != uploader) { + attachment.uploader = uploader + session.saveOrUpdate(attachment) + log.info("Updated attachment $id with uploader $uploader") + attachmentCache.invalidate(id) + attachmentContentCache.invalidate(id) + } + } + throw DuplicateAttachmentException(id.toString()) } } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt index d780b37033..9eac667dc1 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt @@ -9,6 +9,7 @@ import com.nhaarman.mockito_kotlin.whenever import net.corda.core.JarSignatureTestUtils.createJar import net.corda.core.JarSignatureTestUtils.generateKey import net.corda.core.JarSignatureTestUtils.signJar +import net.corda.core.contracts.ContractAttachment import net.corda.core.crypto.SecureHash import net.corda.core.crypto.sha256 import net.corda.core.flows.FlowLogic @@ -48,6 +49,7 @@ import javax.tools.StandardLocation import javax.tools.ToolProvider import kotlin.test.assertEquals import kotlin.test.assertFailsWith +import kotlin.test.assertNotEquals import kotlin.test.assertNull @@ -124,6 +126,30 @@ class NodeAttachmentServiceTest { } } + @Test + fun `insert contract attachment as an untrusted uploader and then as trusted CorDapp uploader`() { + val contractJarName = makeTestContractJar("com.example.MyContract") + val testJar = dir.resolve(contractJarName) + val expectedHash = testJar.readAll().sha256() + + // PRIVILEGED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER, P2P_UPLOADER, UNKNOWN_UPLOADER) + // TRUSTED_UPLOADERS = listOf(DEPLOYED_CORDAPP_UPLOADER, RPC_UPLOADER) + + database.transaction { + val id = testJar.read { storage.privilegedImportOrGetAttachment(it, P2P_UPLOADER, null) } + assertEquals(expectedHash, id) + val attachment1 = storage.openAttachment(expectedHash) + + val id2 = testJar.read { storage.privilegedImportOrGetAttachment(it, DEPLOYED_CORDAPP_UPLOADER, null) } + assertEquals(expectedHash, id2) + val attachment2 = storage.openAttachment(expectedHash) + + assertNotEquals(attachment1, attachment2) + assertEquals(P2P_UPLOADER, (attachment1 as ContractAttachment).uploader) + assertEquals(DEPLOYED_CORDAPP_UPLOADER, (attachment2 as ContractAttachment).uploader) + } + } + @Test fun `missing is not cached`() { val (testJar, expectedHash) = makeTestJar() diff --git a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt index 01386d8823..7f77351952 100644 --- a/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt +++ b/serialization-deterministic/src/main/kotlin/net/corda/serialization/internal/AttachmentsClassLoaderBuilder.kt @@ -1,13 +1,12 @@ package net.corda.serialization.internal import net.corda.core.crypto.SecureHash -import java.lang.ClassLoader /** * Drop-in replacement for [AttachmentsClassLoaderBuilder] in the serialization module. * This version is not strongly-coupled to [net.corda.core.node.ServiceHub]. */ @Suppress("UNUSED", "UNUSED_PARAMETER") -internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { - fun build(attachmentHashes: List): AttachmentsClassLoader? = null +internal class AttachmentsClassLoaderBuilder() { + fun build(attachmentHashes: List, properties: Map, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? = null } \ No newline at end of file diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt index c370084e7a..12519312e9 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/CheckpointSerializationScheme.kt @@ -2,8 +2,11 @@ package net.corda.serialization.internal import net.corda.core.KeepForDJVM import net.corda.core.crypto.SecureHash -import net.corda.core.serialization.* +import net.corda.core.serialization.ClassWhitelist +import net.corda.core.serialization.EncodingWhitelist +import net.corda.core.serialization.SerializationEncoding import net.corda.core.serialization.internal.CheckpointSerializationContext +import java.lang.UnsupportedOperationException @KeepForDJVM data class CheckpointSerializationContextImpl @JvmOverloads constructor( @@ -13,17 +16,13 @@ data class CheckpointSerializationContextImpl @JvmOverloads constructor( override val objectReferencesEnabled: Boolean, override val encoding: SerializationEncoding?, override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist) : CheckpointSerializationContext { - private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader) - /** * {@inheritDoc} * - * We need to cache the AttachmentClassLoaders to avoid too many contexts, since the class loader is part of cache key for the context. + * Unsupported for checkpoints. */ override fun withAttachmentsClassLoader(attachmentHashes: List): CheckpointSerializationContext { - properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this - val classLoader = builder.build(attachmentHashes) ?: return this - return withClassLoader(classLoader) + throw UnsupportedOperationException() } override fun withProperty(property: Any, value: Any): CheckpointSerializationContext { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt index 9c78e01926..ed5aecd987 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/SerializationScheme.kt @@ -31,8 +31,10 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe override val useCase: SerializationContext.UseCase, override val encoding: SerializationEncoding?, override val encodingWhitelist: EncodingWhitelist = NullEncodingWhitelist, - override val lenientCarpenterEnabled: Boolean = false) : SerializationContext { - private val builder = AttachmentsClassLoaderBuilder(properties, deserializationClassLoader) + override val lenientCarpenterEnabled: Boolean = false, + private val builder: AttachmentsClassLoaderBuilder = AttachmentsClassLoaderBuilder() +) : SerializationContext { + /** * {@inheritDoc} @@ -41,7 +43,7 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe */ override fun withAttachmentsClassLoader(attachmentHashes: List): SerializationContext { properties[attachmentsClassLoaderEnabledPropertyName] as? Boolean == true || return this - val classLoader = builder.build(attachmentHashes) ?: return this + val classLoader = builder.build(attachmentHashes, properties, deserializationClassLoader) ?: return this return withClassLoader(classLoader) } @@ -75,13 +77,13 @@ data class SerializationContextImpl @JvmOverloads constructor(override val prefe * can replace it with an alternative version. */ @DeleteForDJVM -internal class AttachmentsClassLoaderBuilder(private val properties: Map, private val deserializationClassLoader: ClassLoader) { - private val cache: Cache, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() +class AttachmentsClassLoaderBuilder() { + private val cache: Cache, ClassLoader>, AttachmentsClassLoader> = Caffeine.newBuilder().weakValues().maximumSize(1024).build() - fun build(attachmentHashes: List): AttachmentsClassLoader? { + fun build(attachmentHashes: List, properties: Map, deserializationClassLoader: ClassLoader): AttachmentsClassLoader? { val serializationContext = properties[serializationContextKey] as? SerializeAsTokenContext ?: return null // Some tests don't set one. try { - return cache.get(attachmentHashes) { + return cache.get(Pair(attachmentHashes, deserializationClassLoader)) { val missing = ArrayList() val attachments = ArrayList() attachmentHashes.forEach { id -> @@ -120,12 +122,13 @@ open class SerializationFactoryImpl( // truncate sequence to at most magicSize, and make sure it's a copy to avoid holding onto large ByteArrays val magic = CordaSerializationMagic(byteSequence.slice(end = magicSize).copyBytes()) val lookupKey = magic to target - return schemes.computeIfAbsent(lookupKey) { + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return (schemes[lookupKey] ?: schemes.computeIfAbsent(lookupKey) { registeredSchemes.filter { it.canDeserializeVersion(magic, target) }.forEach { return@computeIfAbsent it } // XXX: Not single? logger.warn("Cannot find serialization scheme for: [$lookupKey, " + "${if (magic == amqpMagic) "AMQP" else "UNKNOWN MAGIC"}] registeredSchemes are: $registeredSchemes") throw UnsupportedOperationException("Serialization scheme $lookupKey not supported.") - } to magic + }) to magic } @Throws(NotSerializableException::class) diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt index f6b5556f82..5be95c4306 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/AMQPSerializationScheme.kt @@ -40,12 +40,19 @@ interface SerializerFactoryFactory { @KeepForDJVM abstract class AbstractAMQPSerializationScheme( private val cordappCustomSerializers: Set>, - private val serializerFactoriesForContexts: AccessOrderLinkedHashMap, SerializerFactory>, + maybeNotConcurrentSerializerFactoriesForContexts: MutableMap, SerializerFactory>, val sff: SerializerFactoryFactory = createSerializerFactoryFactory() ) : SerializationScheme { @DeleteForDJVM constructor(cordapps: List) : this(cordapps.customSerializers, AccessOrderLinkedHashMap(128)) + // This is a bit gross but a broader check for ConcurrentMap is not allowed inside DJVM. + private val serializerFactoriesForContexts: MutableMap, SerializerFactory> = if (maybeNotConcurrentSerializerFactoriesForContexts is AccessOrderLinkedHashMap<*, *>) { + Collections.synchronizedMap(maybeNotConcurrentSerializerFactoriesForContexts) + } else { + maybeNotConcurrentSerializerFactoriesForContexts + } + // TODO: This method of initialisation for the Whitelist and plugin serializers will have to change // when we have per-cordapp contexts and dynamic app reloading but for now it's the easiest way companion object { @@ -166,8 +173,9 @@ abstract class AbstractAMQPSerializationScheme( open val publicKeySerializer: CustomSerializer<*> = net.corda.serialization.internal.amqp.custom.PublicKeySerializer private fun getSerializerFactory(context: SerializationContext): SerializerFactory { - return synchronized(serializerFactoriesForContexts) { - serializerFactoriesForContexts.computeIfAbsent(Pair(context.whitelist, context.deserializationClassLoader)) { + val key = Pair(context.whitelist, context.deserializationClassLoader) + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return serializerFactoriesForContexts[key] ?: serializerFactoriesForContexts.computeIfAbsent(key) { when (context.useCase) { SerializationContext.UseCase.RPCClient -> rpcClientSerializerFactory(context) @@ -178,7 +186,6 @@ abstract class AbstractAMQPSerializationScheme( registerCustomSerializers(context, it) } } - } } override fun deserialize(byteSequence: ByteSequence, clazz: Class, context: SerializationContext): T { diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt index cfce4fa76e..c8d4b14891 100644 --- a/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/amqp/SerializerFactory.kt @@ -7,7 +7,10 @@ import net.corda.core.StubOutForDJVM import net.corda.core.internal.kotlinObjectInstance import net.corda.core.internal.uncheckedCast import net.corda.core.serialization.ClassWhitelist -import net.corda.core.utilities.* +import net.corda.core.utilities.contextLogger +import net.corda.core.utilities.debug +import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import net.corda.serialization.internal.carpenter.* import org.apache.qpid.proton.amqp.* import java.io.NotSerializableException @@ -95,6 +98,9 @@ open class SerializerFactory( val classloader: ClassLoader get() = classCarpenter.classloader + // Used to short circuit any computation for a given input, for performance. + private data class MemoType(val actualClass: Class<*>?, val declaredType: Type) : Type + /** * Look up, and manufacture if necessary, a serializer for the given type. * @@ -106,50 +112,56 @@ open class SerializerFactory( // can be useful to enable but will be *extremely* chatty if you do logger.trace { "Get Serializer for $actualClass ${declaredType.typeName}" } - val declaredClass = declaredType.asClass() - val actualType: Type = if (actualClass == null) declaredType - else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + val ourType = MemoType(actualClass, declaredType) + // ConcurrentHashMap.get() is lock free, but computeIfAbsent is not, even if the key is in the map already. + return serializersByType[ourType] ?: run { - val serializer = when { + val declaredClass = declaredType.asClass() + val actualType: Type = if (actualClass == null) declaredType + else inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType + + val serializer = when { // Declared class may not be set to Collection, but actual class could be a collection. // In this case use of CollectionSerializer is perfectly appropriate. - (Collection::class.java.isAssignableFrom(declaredClass) || - (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && - !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { - val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) - serializersByType.computeIfAbsent(declaredTypeAmended) { - CollectionSerializer(declaredTypeAmended, this) + (Collection::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Collection::class.java.isAssignableFrom(actualClass))) && + !EnumSet::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { + val declaredTypeAmended = CollectionSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredTypeAmended) { + CollectionSerializer(declaredTypeAmended, this) + } } - } // Declared class may not be set to Map, but actual class could be a map. // In this case use of MapSerializer is perfectly appropriate. - (Map::class.java.isAssignableFrom(declaredClass) || - (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { - val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) - serializersByType.computeIfAbsent(declaredTypeAmended) { - makeMapSerializer(declaredTypeAmended) - } - } - Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { - logger.trace { - "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + - "declaredType=${declaredType.typeName} " + - "isEnum=${declaredType::class.java.isEnum}" + (Map::class.java.isAssignableFrom(declaredClass) || + (actualClass != null && Map::class.java.isAssignableFrom(actualClass))) -> { + val declaredTypeAmended = MapSerializer.deriveParameterizedType(declaredType, declaredClass, actualClass) + serializersByType.computeIfAbsent(declaredTypeAmended) { + makeMapSerializer(declaredTypeAmended) + } } + Enum::class.java.isAssignableFrom(actualClass ?: declaredClass) -> { + logger.trace { + "class=[${actualClass?.simpleName} | $declaredClass] is an enumeration " + + "declaredType=${declaredType.typeName} " + + "isEnum=${declaredType::class.java.isEnum}" + } - serializersByType.computeIfAbsent(actualClass ?: declaredClass) { - whitelist.requireWhitelisted(actualType) - EnumSerializer(actualType, actualClass ?: declaredClass, this) + serializersByType.computeIfAbsent(actualClass ?: declaredClass) { + whitelist.requireWhitelisted(actualType) + EnumSerializer(actualType, actualClass ?: declaredClass, this) + } + } + else -> { + makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) } } - else -> { - makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType) - } + + serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer) + // Always store the short-circuit too, for performance. + serializersByType.putIfAbsent(ourType, serializer) + return serializer } - - serializersByDescriptor.putIfAbsent(serializer.typeDescriptor, serializer) - - return serializer } /** diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt index 526c52c1c7..800f5e87c7 100644 --- a/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/amqp/StaticInitialisationOfSerializedObjectTest.kt @@ -63,9 +63,9 @@ class StaticInitialisationOfSerializedObjectTest { // we can't actually construct one sf.get(null, D::class.java) - // post creation of the serializer we should have one element in the map, this + // post creation of the serializer we should have two elements in the map, this // proves we didn't statically construct an instance of C when building the serializer - assertEquals(1, serialisersByType.size) + assertEquals(2, serialisersByType.size) } @Test diff --git a/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java index 204a9529fa..97bbc243aa 100644 --- a/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java +++ b/tools/network-bootstrapper/src/main/java/net/corda/bootstrapper/GuiUtils.java @@ -1,5 +1,7 @@ package net.corda.bootstrapper; +import javafx.application.Platform; +import javafx.embed.swing.JFXPanel; import javafx.scene.control.Alert; import javafx.scene.control.Label; import javafx.scene.control.TextArea; @@ -9,6 +11,7 @@ import javafx.stage.StageStyle; import java.io.PrintWriter; import java.io.StringWriter; +import java.util.concurrent.CountDownLatch; public class GuiUtils { @@ -18,31 +21,40 @@ public class GuiUtils { alert.setTitle("Exception"); alert.setHeaderText(title); alert.setContentText(message); - - StringWriter sw = new StringWriter(); - PrintWriter pw = new PrintWriter(sw); - exception.printStackTrace(pw); - String exceptionText = sw.toString(); - - Label label = new Label("Details:"); - - TextArea textArea = new TextArea(exceptionText); - textArea.setEditable(false); - textArea.setWrapText(true); - - textArea.setMaxWidth(Double.MAX_VALUE); - textArea.setMaxHeight(Double.MAX_VALUE); - GridPane.setVgrow(textArea, Priority.ALWAYS); - GridPane.setHgrow(textArea, Priority.ALWAYS); - - GridPane expContent = new GridPane(); - expContent.setMaxWidth(Double.MAX_VALUE); - expContent.add(label, 0, 0); - expContent.add(textArea, 0, 1); - - alert.getDialogPane().setExpandableContent(expContent); - + if (exception != null) { + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw); + exception.printStackTrace(pw); + String exceptionText = sw.toString(); + TextArea textArea = new TextArea(exceptionText); + textArea.setEditable(false); + textArea.setWrapText(true); + textArea.setMaxWidth(Double.MAX_VALUE); + textArea.setMaxHeight(Double.MAX_VALUE); + Label label = new Label("Details:"); + GridPane.setVgrow(textArea, Priority.ALWAYS); + GridPane.setHgrow(textArea, Priority.ALWAYS); + GridPane expContent = new GridPane(); + expContent.setMaxWidth(Double.MAX_VALUE); + expContent.add(label, 0, 0); + expContent.add(textArea, 0, 1); + alert.getDialogPane().setExpandableContent(expContent); + } alert.showAndWait(); } + public static void showAndQuit(String title, String message, Throwable exception) { + CountDownLatch countDownLatch = new CountDownLatch(1); + new JFXPanel(); + Platform.runLater(() -> { + showException(title, message, exception); + countDownLatch.countDown(); + System.exit(1); + }); + try { + countDownLatch.await(); + } catch (InterruptedException e) { + } + } + } diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt index eb38bf11cb..76623cd21c 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/Main.kt @@ -1,4 +1,5 @@ @file:JvmName("Main") + package net.corda.bootstrapper import javafx.application.Application @@ -7,15 +8,19 @@ import net.corda.bootstrapper.backends.Backend.BackendType.AZURE import net.corda.bootstrapper.cli.AzureParser import net.corda.bootstrapper.cli.CliParser import net.corda.bootstrapper.cli.CommandLineInterface +import net.corda.bootstrapper.docker.DockerUtils import net.corda.bootstrapper.gui.Gui import net.corda.bootstrapper.serialization.SerializationEngine import picocli.CommandLine +import javax.ws.rs.ProcessingException +import kotlin.system.exitProcess val baseArgs = CliParser() fun main(args: Array) { SerializationEngine.init() CommandLine(baseArgs).parse(*args) + testDockerConnectivity() if (baseArgs.gui) { Application.launch(Gui::class.java) @@ -32,3 +37,16 @@ fun main(args: Array) { } CommandLineInterface().run(argParser) } + +private fun testDockerConnectivity() { + try { + DockerUtils.createLocalDockerClient().listImagesCmd().exec() + } catch (se: ProcessingException) { + if (baseArgs.gui) { + GuiUtils.showAndQuit("Could not connect to Docker", "Please ensure that docker is running locally", null) + } else { + System.err.println("Could not connect to Docker, please ensure that docker is running locally") + exitProcess(1) + } + } +} diff --git a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt index 841022f502..a517ba7599 100644 --- a/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt +++ b/tools/network-bootstrapper/src/main/kotlin/net/corda/bootstrapper/gui/Gui.kt @@ -1,7 +1,7 @@ package net.corda.bootstrapper.gui import javafx.stage.Stage -import tornadofx.* +import tornadofx.App class Gui : App(BootstrapperView::class) { override fun start(stage: Stage) {