diff --git a/README.md b/README.md
index 1601315cc8..eba677a651 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,6 @@
-
+
+
+
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) {