diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6891e6e828..708384f2c6 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -52,6 +52,12 @@
+
+
+
+
+
+
@@ -98,6 +104,8 @@
+
+
diff --git a/docs/source/running-doorman.rst b/docs/source/running-doorman.rst
index 451fb1f033..0b920b83d6 100644
--- a/docs/source/running-doorman.rst
+++ b/docs/source/running-doorman.rst
@@ -39,6 +39,8 @@ Allowed parameters are:
:approveInterval: How often to process Jira approved requests in seconds.
+ :crlEndpoint: URL to the CRL issued by the Doorman CA. This parameter is only useful when Doorman is executing in the local signing mode.
+
:jira: The Jira configuration for certificate signing requests
:address: The URL to use to connect to Jira
@@ -51,7 +53,7 @@ Allowed parameters are:
:revocation: Revocation service specific configuration
- :localSigning: Configuration for local CRL signing using the file key store. If not defined t
+ :localSigning: Configuration for local CRL signing using the file key store. If not defined then an external signing process is assumed.
:crlUpdateInterval: Validity time of the issued certificate revocation lists (in milliseconds).
@@ -66,6 +68,12 @@ Allowed parameters are:
:approveAll: Whether to approve all requests (defaults to false), this is for debug only.
+ :caCrlPath: Path (including the file name) to the location of the file containing the bytes of the CRL issued by the ROOT CA.
+ Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
+
+ :emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA.
+ Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
+
:jira: The Jira configuration for certificate revocation requests
:address: The URL to use to connect to Jira
@@ -88,6 +96,22 @@ Allowed parameters are:
:rootStorePath: Path for the root keystore
+:caCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the CRL issued by the ROOT CA.
+ This configuration parameter is used in the ROOT_KEYGEN mode.
+ Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
+
+:caCrlUrl: URL to the CRL issued by the ROOT CA. This URL is going to be included in the generated CRL that is signed by the ROOT CA.
+ This configuration parameter is used in the ROOT_KEYGEN and CA_KEYGEN modes.
+
+:emptyCrlPath: Path (including the file name) to the location of the generated file containing the bytes of the empty CRL issued by the ROOT CA.
+ This configuration parameter is used in the ROOT_KEYGEN mode.
+ Note: Byte encoding is the one given by the package java.security.cert.X509CRL.encoded method - i.e. ASN.1 DER
+ This CRL is to allow nodes to operate in the strict CRL checking mode. This mode requires all the certificates in the chain being validated
+ to point a CRL. Since the TLS-level certificate is managed by the nodes, this CRL is a facility one for infrastructures without CRL provisioning.
+
+:emptyCrlUrl: URL to the empty CRL issued by the ROOT CA. This URL is going to be included in the generated empty CRL that is signed by the ROOT CA.
+ This configuration parameter is used in the ROOT_KEYGEN mode.
+
Bootstrapping the network parameters
------------------------------------
When doorman is running it will serve the current network parameters. The first time doorman is
diff --git a/launcher/build.gradle b/launcher/build.gradle
new file mode 100644
index 0000000000..1c65f08593
--- /dev/null
+++ b/launcher/build.gradle
@@ -0,0 +1,19 @@
+group 'com.r3.corda'
+version 'R3.CORDA-3.0-SNAPSHOT'
+
+apply plugin: 'java'
+apply plugin: 'kotlin'
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
+ compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
+}
+
+ext {
+ loaderClassName = "net.corda.launcher.Loader"
+ launcherClassName = "net.corda.launcher.Launcher"
+}
+
+jar {
+ baseName 'corda-launcher'
+}
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
new file mode 100644
index 0000000000..c9f9bffec1
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Launcher.kt
@@ -0,0 +1,81 @@
+@file:JvmName("Launcher")
+
+package net.corda.launcher
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import kotlin.system.exitProcess
+
+fun main(args: Array) {
+
+ if(args.isEmpty()) {
+ println("Usage: launcher [args]")
+ exitProcess(0)
+ }
+
+ // TODO: --base-directory is specific of the Node app, it should be controllable by a config property
+ val nodeBaseDir = Paths.get(Settings.WORKING_DIR)
+ .resolve(getBaseDirectory(args) ?: ".")
+ .toAbsolutePath()
+
+ val appClassLoader = setupClassLoader(nodeBaseDir)
+
+ val appMain = try {
+ appClassLoader
+ .loadClass(args[0])
+ .getMethod("main", Array::class.java)
+ } catch (e: Exception) {
+ System.err.println("Error looking for method 'main' in class ${args[0]}:")
+ e.printStackTrace()
+ exitProcess(1)
+ }
+
+ // Propagate current working directory via system property, to patch it after javapackager
+ System.setProperty("corda-launcher.cwd", Settings.WORKING_DIR)
+ System.setProperty("user.dir", Settings.WORKING_DIR)
+
+ try {
+ appMain.invoke(null, args.sliceArray(1..args.lastIndex))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ exitProcess(1)
+ }
+}
+
+private fun setupClassLoader(nodeBaseDir: Path): ClassLoader {
+ val sysClassLoader = ClassLoader.getSystemClassLoader()
+
+ val appClassLoader = (sysClassLoader as? Loader) ?: {
+ println("WARNING: failed to override system classloader")
+ Loader(sysClassLoader)
+ } ()
+
+ // Lookup plugins and extend classpath
+ val pluginURLs = Settings.PLUGINS.flatMap {
+ val entry = nodeBaseDir.resolve(it)
+ if (Files.isDirectory(entry)) {
+ entry.jarFiles()
+ } else {
+ setOf(entry)
+ }
+ }.map { it.toUri().toURL() }
+
+ appClassLoader.augmentClasspath(pluginURLs)
+
+ // For logging
+ System.setProperty("corda-launcher.appclassloader.urls", appClassLoader.urLs.joinToString(":"))
+
+ return appClassLoader
+}
+
+private fun getBaseDirectory(args: Array): String? {
+ val idx = args.indexOf("--base-directory")
+ return if (idx != -1 && idx < args.lastIndex) {
+ args[idx + 1]
+ } else null
+}
+
+private fun Path.jarFiles(): List {
+ return Files.newDirectoryStream(this).filter { it.toString().endsWith(".jar") }
+}
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Loader.kt b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt
new file mode 100644
index 0000000000..49e35d3531
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Loader.kt
@@ -0,0 +1,11 @@
+package net.corda.launcher
+
+import java.net.URL
+import java.net.URLClassLoader
+
+class Loader(parent: ClassLoader?): URLClassLoader(Settings.CLASSPATH.toTypedArray(), parent) {
+
+ fun augmentClasspath(urls: List) {
+ urls.forEach { addURL(it) }
+ }
+}
diff --git a/launcher/src/main/kotlin/net/corda/launcher/Settings.kt b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt
new file mode 100644
index 0000000000..74bc91f79b
--- /dev/null
+++ b/launcher/src/main/kotlin/net/corda/launcher/Settings.kt
@@ -0,0 +1,59 @@
+package net.corda.launcher
+
+import java.io.FileInputStream
+import java.net.URL
+import java.nio.file.Files
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.util.*
+import kotlin.collections.HashSet
+
+// Expose Corda bootstrapping settings from property file
+object Settings {
+
+ // JavaPackager reset cwd to the "/apps" subfolder, so its location is in the parent directory
+ private val LAUNCHER_PATH = Paths.get("..")
+
+ // Launcher property file
+ private val CORDA_RUNTIME_SETTINGS = LAUNCHER_PATH.resolve("runtime.properties")
+
+ // The application working directory
+ val WORKING_DIR: String = System.getenv("CORDA_LAUNCHER_CWD") ?: ".."
+
+ // Application classpath
+ val CLASSPATH: List
+
+ // Plugin directories (all contained jar files are added to classpath)
+ val PLUGINS: List
+
+ // Path of the "lib" subdirectory in bundle
+ private val LIBPATH: Path
+
+ init {
+ val settings = Properties().apply {
+ load(FileInputStream(CORDA_RUNTIME_SETTINGS.toFile()))
+ }
+
+ LIBPATH = Paths.get(settings.getProperty("libpath") ?: ".")
+ CLASSPATH = parseClasspath(settings)
+ PLUGINS = parsePlugins(settings)
+ }
+
+ private fun parseClasspath(config: Properties): List {
+ val libDir = LAUNCHER_PATH.resolve(LIBPATH).toAbsolutePath()
+ val cp = config.getProperty("classpath") ?:
+ throw Error("Missing 'classpath' property from config")
+
+ return cp.split(':').map {
+ libDir.resolve(it).toUri().toURL()
+ }
+ }
+
+ private fun parsePlugins(config: Properties): List {
+ val ext = config.getProperty("plugins")
+
+ return ext?.let {
+ it.split(':').map { Paths.get(it) }
+ } ?: emptyList()
+ }
+}
diff --git a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt
index 76ed6803b1..2ff23b6295 100644
--- a/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt
+++ b/network-management/registration-tool/src/main/kotlin/com/r3/corda/networkmanage/registration/NotaryRegistrationTool.kt
@@ -68,4 +68,5 @@ data class NotaryRegistrationConfig(val legalName: CordaX500Name,
val networkRootTrustStorePassword: String?,
val trustStorePassword: String?,
val keystorePath: Path?,
- val crlCheckSoftFail: Boolean)
+ val crlCheckSoftFail: Boolean,
+ val crlDistributionPoint: URL? = null)
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt
index fcb5e24906..623be2f799 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/HsmBaseTest.kt
@@ -26,7 +26,7 @@ import net.corda.nodeapi.internal.crypto.CertificateType
import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.testing.internal.IntegrationTest
import net.corda.testing.internal.IntegrationTestSchemas
-import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
+import net.corda.testing.node.internal.makeTestDataSourceProperties
import net.corda.testing.node.internal.makeTestDatabaseProperties
import org.junit.Before
import org.junit.ClassRule
@@ -185,10 +185,10 @@ abstract class HsmBaseTest : IntegrationTest() {
}
fun makeTestDataSourceProperties(): Properties {
- return makeTestDataSourceProperties(DOORMAN_DB_NAME)
+ return makeTestDataSourceProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases())
}
fun makeTestDatabaseProperties(): DatabaseConfig {
- return makeTestDatabaseProperties(DOORMAN_DB_NAME)
+ return makeTestDatabaseProperties(DOORMAN_DB_NAME, configSupplier = configSupplierForSupportedDatabases())
}
}
\ No newline at end of file
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt
index ce84d4c3b2..b8dc16810e 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/common/TestUtils.kt
@@ -10,16 +10,26 @@
package com.r3.corda.networkmanage.common
+import com.r3.corda.networkmanage.common.utils.createSignedCrl
+import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import net.corda.core.crypto.SecureHash
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.core.utilities.days
+import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.testing.database.DatabaseConstants
+import net.corda.testing.node.internal.databaseProviderDataSourceConfig
+import org.apache.commons.io.FileUtils
+import org.junit.rules.TemporaryFolder
+import java.net.URL
+import java.nio.file.Path
const val HOST = "localhost"
const val DOORMAN_DB_NAME = "doorman"
-fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null) : Config {
+fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: String? = null): Config {
val nodeName = nodeName ?: SecureHash.randomSHA256().toString()
val h2InstanceName = if (postfix != null) nodeName + "_" + postfix else nodeName
@@ -28,4 +38,26 @@ fun networkMapInMemoryH2DataSourceConfig(nodeName: String? = null, postfix: Stri
DatabaseConstants.DATA_SOURCE_URL to "jdbc:h2:mem:${h2InstanceName};DB_CLOSE_DELAY=-1",
DatabaseConstants.DATA_SOURCE_USER to "sa",
DatabaseConstants.DATA_SOURCE_PASSWORD to ""))
-}
\ No newline at end of file
+}
+
+fun generateEmptyCrls(tempFolder: TemporaryFolder, rootCertAndKeyPair: CertificateAndKeyPair, directEndpoint: URL, indirectEndpoint: URL): Pair {
+ val localSigner = LocalSigner(rootCertAndKeyPair)
+ val directCrl = createSignedCrl(rootCertAndKeyPair.certificate, directEndpoint, 10.days, localSigner, emptyList(), false)
+ val indirectCrl = createSignedCrl(rootCertAndKeyPair.certificate, indirectEndpoint, 10.days, localSigner, emptyList(), true)
+ val directCrlFile = tempFolder.newFile()
+ FileUtils.writeByteArrayToFile(directCrlFile, directCrl.encoded)
+ val indirectCrlFile = tempFolder.newFile()
+ FileUtils.writeByteArrayToFile(indirectCrlFile, indirectCrl.encoded)
+ return Pair(directCrlFile.toPath(), indirectCrlFile.toPath())
+}
+
+fun getCaCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/root")
+fun getEmptyCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/empty")
+fun getNodeCrlEndpoint(serverAddress: NetworkHostAndPort) = URL("http://$serverAddress/certificate-revocation-list/doorman")
+
+//TODO add more dbs to test once doorman supports them
+fun configSupplierForSupportedDatabases(): (String?, String?) -> Config =
+ when (System.getProperty("custom.databaseProvider", "")) {
+ "integration-sql-server", "integration-azure-sql" -> ::databaseProviderDataSourceConfig
+ else -> { _, _ -> ConfigFactory.empty() }
+ }
\ No newline at end of file
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt
index 993985a985..2049753f83 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NetworkParametersUpdateTest.kt
@@ -1,6 +1,7 @@
package com.r3.corda.networkmanage.doorman
import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME
+import com.r3.corda.networkmanage.common.configSupplierForSupportedDatabases
import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
@@ -154,7 +155,8 @@ class NetworkParametersUpdateTest : IntegrationTest() {
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
val doormanConfig = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
- val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), DatabaseConfig(runMigration = true), doormanConfig, null)
+ val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
+ DatabaseConfig(runMigration = true), doormanConfig, null)
server.start(
serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
@@ -173,7 +175,7 @@ class NetworkParametersUpdateTest : IntegrationTest() {
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close()
NetworkManagementServer(
- makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
+ makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
DatabaseConfig(runMigration = true),
DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis),
null).use {
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
index fa1be7a347..955d25871f 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/doorman/NodeRegistrationTest.kt
@@ -10,8 +10,7 @@
package com.r3.corda.networkmanage.doorman
-import com.r3.corda.networkmanage.common.DOORMAN_DB_NAME
-import com.r3.corda.networkmanage.common.networkMapInMemoryH2DataSourceConfig
+import com.r3.corda.networkmanage.common.*
import com.r3.corda.networkmanage.common.utils.CertPathAndKey
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.cordform.CordformNode
@@ -43,7 +42,9 @@ import net.corda.testing.node.internal.makeTestDataSourceProperties
import net.corda.testing.node.internal.makeTestDatabaseProperties
import org.assertj.core.api.Assertions.assertThat
import org.junit.*
+import org.junit.rules.TemporaryFolder
import java.net.URL
+import java.nio.file.Path
import java.security.cert.X509Certificate
import kotlin.streams.toList
@@ -77,17 +78,25 @@ class NodeRegistrationTest : IntegrationTest() {
private var server: NetworkManagementServer? = null
+ @Rule
+ @JvmField
+ val tempFolder = TemporaryFolder()
+
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, jira = null, approveInterval = timeoutMillis)
- private val revocationConfig: CertificateRevocationConfig
- get() = CertificateRevocationConfig(
+ private lateinit var revocationConfig: CertificateRevocationConfig
+
+ private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
+ return CertificateRevocationConfig(
approveAll = true,
jira = null,
approveInterval = timeoutMillis,
crlCacheTimeout = timeoutMillis,
localSigning = CertificateRevocationConfig.LocalSigning(
- crlEndpoint = URL("http://test.com/crl"),
- crlUpdateInterval = timeoutMillis)
- )
+ crlEndpoint = getNodeCrlEndpoint(serverAddress),
+ crlUpdateInterval = timeoutMillis),
+ emptyCrlPath = emptyCrlPath,
+ caCrlPath = caCrlPath)
+ }
@Before
fun init() {
@@ -96,6 +105,8 @@ class NodeRegistrationTest : IntegrationTest() {
rootCaCert = rootCa.certificate
this.doormanCa = doormanCa
networkMapCa = createDevNetworkMapCa(rootCa)
+ val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
+ revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
}
@After
@@ -174,7 +185,8 @@ class NodeRegistrationTest : IntegrationTest() {
}
private fun startServer(startNetworkMap: Boolean = true): NetworkManagementServer {
- val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), makeTestDatabaseProperties(DOORMAN_DB_NAME), doormanConfig, revocationConfig)
+ val server = NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
+ makeTestDatabaseProperties(configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig)
server.start(
serverAddress,
CertPathAndKey(listOf(doormanCa.certificate, rootCaCert), doormanCa.keyPair.private),
@@ -192,7 +204,8 @@ class NodeRegistrationTest : IntegrationTest() {
private fun applyNetworkParametersAndStart(networkParametersCmd: NetworkParametersCmd) {
server?.close()
- NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig), makeTestDatabaseProperties(DOORMAN_DB_NAME), doormanConfig, revocationConfig).use {
+ NetworkManagementServer(makeTestDataSourceProperties(DOORMAN_DB_NAME, dbNamePostfix, configSupplier = configSupplierForSupportedDatabases(), fallBackConfigSupplier = ::networkMapInMemoryH2DataSourceConfig),
+ makeTestDatabaseProperties(configSupplier = configSupplierForSupportedDatabases()), doormanConfig, revocationConfig).use {
it.netParamsUpdateHandler.processNetworkParameters(networkParametersCmd)
}
server = startServer(startNetworkMap = true)
diff --git a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
index e7db87b120..db7d13bdb4 100644
--- a/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
+++ b/network-management/src/integration-test/kotlin/com/r3/corda/networkmanage/hsm/SigningServiceIntegrationTest.kt
@@ -11,8 +11,7 @@
package com.r3.corda.networkmanage.hsm
import com.nhaarman.mockito_kotlin.*
-import com.r3.corda.networkmanage.common.HOST
-import com.r3.corda.networkmanage.common.HsmBaseTest
+import com.r3.corda.networkmanage.common.*
import com.r3.corda.networkmanage.common.persistence.configureDatabase
import com.r3.corda.networkmanage.doorman.CertificateRevocationConfig
import com.r3.corda.networkmanage.doorman.DoormanConfig
@@ -26,9 +25,6 @@ import net.corda.core.identity.CordaX500Name
import net.corda.core.internal.createDirectories
import net.corda.core.internal.div
import net.corda.core.internal.uncheckedCast
-import net.corda.core.utilities.NetworkHostAndPort
-import net.corda.core.utilities.hours
-import net.corda.core.utilities.minutes
import net.corda.core.utilities.seconds
import net.corda.node.NodeRegistrationOption
import net.corda.node.services.config.NodeConfiguration
@@ -40,6 +36,7 @@ import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule
+import net.corda.testing.driver.PortAllocation
import net.corda.testing.internal.createDevIntermediateCaCertPath
import net.corda.testing.internal.rigorousMock
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequest
@@ -48,6 +45,7 @@ import org.junit.Before
import org.junit.Rule
import org.junit.Test
import java.net.URL
+import java.nio.file.Path
import java.security.cert.X509Certificate
import java.util.*
import javax.persistence.PersistenceException
@@ -59,25 +57,31 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
@JvmField
val testSerialization = SerializationEnvironmentRule(true)
+ private val portAllocation = PortAllocation.Incremental(10000)
+ private val serverAddress = portAllocation.nextHostAndPort()
+
private lateinit var timer: Timer
private lateinit var rootCaCert: X509Certificate
private lateinit var intermediateCa: CertificateAndKeyPair
+ private val timeoutMillis = 5.seconds.toMillis()
private lateinit var dbName: String
private val doormanConfig: DoormanConfig get() = DoormanConfig(approveAll = true, approveInterval = 2.seconds.toMillis(), jira = null)
- private val revocationConfig: CertificateRevocationConfig
- get() = CertificateRevocationConfig(
+ private lateinit var revocationConfig: CertificateRevocationConfig
+
+ private fun createCertificateRevocationConfig(emptyCrlPath: Path, caCrlPath: Path): CertificateRevocationConfig {
+ return CertificateRevocationConfig(
approveAll = true,
jira = null,
- crlCacheTimeout = 30.minutes.toMillis(),
- approveInterval = 10.minutes.toMillis(),
+ approveInterval = timeoutMillis,
+ crlCacheTimeout = timeoutMillis,
localSigning = CertificateRevocationConfig.LocalSigning(
- crlEndpoint = URL("http://test.com/crl"),
- crlUpdateInterval = 2.hours.toMillis()
- )
- )
-
+ crlEndpoint = getNodeCrlEndpoint(serverAddress),
+ crlUpdateInterval = timeoutMillis),
+ emptyCrlPath = emptyCrlPath,
+ caCrlPath = caCrlPath)
+ }
@Before
override fun setUp() {
@@ -87,6 +91,8 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
val (rootCa, intermediateCa) = createDevIntermediateCaCertPath()
rootCaCert = rootCa.certificate
this.intermediateCa = intermediateCa
+ val (caCrlPath, emptyCrlPath) = generateEmptyCrls(tempFolder, rootCa, getCaCrlEndpoint(serverAddress), getEmptyCrlEndpoint(serverAddress))
+ revocationConfig = createCertificateRevocationConfig(emptyCrlPath, caCrlPath)
}
@After
@@ -116,7 +122,7 @@ class SigningServiceIntegrationTest : HsmBaseTest() {
//Start doorman server
NetworkManagementServer(makeTestDataSourceProperties(), makeTestDatabaseProperties(), doormanConfig, revocationConfig).use { server ->
server.start(
- hostAndPort = NetworkHostAndPort(HOST, 0),
+ hostAndPort = serverAddress,
csrCertPathAndKey = null,
startNetworkMap = null)
val doormanHostAndPort = server.hostAndPort
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt
index 16a5ee5ab3..0782d73754 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorage.kt
@@ -26,7 +26,7 @@ class PersistentCertificateRevocationListStorage(private val database: CordaPers
override fun saveCertificateRevocationList(crl: X509CRL, crlIssuer: CrlIssuer, signedBy: String, revokedAt: Instant) {
database.transaction {
- crl.revokedCertificates.forEach {
+ crl.revokedCertificates?.forEach {
revokeCertificate(it.serialNumber, revokedAt, this)
}
session.save(CertificateRevocationListEntity(
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt
index aef8f8d8e9..42ec4db79b 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorage.kt
@@ -5,6 +5,7 @@ import com.r3.corda.networkmanage.common.persistence.entity.CertificateRevocatio
import com.r3.corda.networkmanage.common.persistence.entity.CertificateSigningRequestEntity
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.CordaX500Name
+import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.internal.network.CertificateRevocationRequest
import net.corda.nodeapi.internal.persistence.CordaPersistence
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
@@ -25,6 +26,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
CRLReason.SUPERSEDED,
CRLReason.UNSPECIFIED
)
+ val logger = contextLogger()
}
override fun saveRevocationRequest(request: CertificateRevocationRequest): String {
@@ -60,7 +62,7 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
}
}
- private fun validate(request:CertificateRevocationRequest) {
+ private fun validate(request: CertificateRevocationRequest) {
require(request.reason in ALLOWED_REASONS) { "The given revocation reason is not allowed." }
}
@@ -140,11 +142,20 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
if (revocation == null) {
throw NoSuchElementException("Error while approving! Certificate revocation id=$id does not exist")
} else {
- session.merge(revocation.copy(
- status = RequestStatus.APPROVED,
- modifiedAt = Instant.now(),
- modifiedBy = approvedBy
- ))
+ when (revocation.status) {
+ RequestStatus.TICKET_CREATED -> {
+ session.merge(revocation.copy(
+ status = RequestStatus.APPROVED,
+ modifiedAt = Instant.now(),
+ modifiedBy = approvedBy
+ ))
+ logger.debug("`request id` = $requestId marked as APPROVED")
+ }
+ else -> {
+ logger.warn("`request id` = $requestId cannot be marked as APPROVED. Its current status is ${revocation.status}")
+ return@transaction
+ }
+ }
}
}
}
@@ -155,27 +166,45 @@ class PersistentCertificateRevocationRequestStorage(private val database: CordaP
if (revocation == null) {
throw NoSuchElementException("Error while rejecting! Certificate revocation id=$id does not exist")
} else {
- session.merge(revocation.copy(
- status = RequestStatus.REJECTED,
- modifiedAt = Instant.now(),
- modifiedBy = rejectedBy,
- remark = reason
- ))
+ when (revocation.status) {
+ RequestStatus.TICKET_CREATED -> {
+ session.merge(revocation.copy(
+ status = RequestStatus.REJECTED,
+ modifiedAt = Instant.now(),
+ modifiedBy = rejectedBy,
+ remark = reason
+ ))
+ logger.debug("`request id` = $requestId marked as REJECTED")
+ }
+ else -> {
+ logger.warn("`request id` = $requestId cannot be marked as REJECTED. Its current status is ${revocation.status}")
+ return@transaction
+ }
+ }
}
}
}
override fun markRequestTicketCreated(requestId: String) {
- // Even though, we have an assumption that there is always a single instance of the doorman service running,
- // the SERIALIZABLE isolation level is used here just to ensure data consistency between the updates.
- return database.transaction(TransactionIsolationLevel.SERIALIZABLE) {
- val request = requireNotNull(getRevocationRequestEntity(requestId, RequestStatus.NEW)) {
- "Error when creating request ticket with id: $requestId. Request does not exist or its status is not NEW."
+ database.transaction {
+ val revocation = getRevocationRequestEntity(requestId)
+ if (revocation == null) {
+ throw NoSuchElementException("Error while marking the request as ticket created! Certificate revocation id=$id does not exist")
+ } else {
+ when (revocation.status) {
+ RequestStatus.NEW -> {
+ session.merge(revocation.copy(
+ modifiedAt = Instant.now(),
+ status = RequestStatus.TICKET_CREATED
+ ))
+ logger.debug("`request id` = $requestId marked as TICKED_CREATED")
+ }
+ else -> {
+ logger.warn("`request id` = $requestId cannot be marked as TICKED_CREATED. Its current status is ${revocation.status}")
+ return@transaction
+ }
+ }
}
- val update = request.copy(
- modifiedAt = Instant.now(),
- status = RequestStatus.TICKET_CREATED)
- session.merge(update)
}
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt
index c8b301bd05..e15cd61822 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateSigningRequestStorage.kt
@@ -177,7 +177,7 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
existingRequestByPubKeyHash?.let {
// Compare subject, attribute.
// We cannot compare the request directly because it contains nonce.
- if (it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) {
+ if (certNotRevoked(it) && it.request.subject == request.subject && it.request.attributes.asList() == request.attributes.asList()) {
return it.requestId
} else {
//TODO Consider following scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
@@ -190,11 +190,19 @@ class PersistentCertificateSigningRequestStorage(private val database: CordaPers
// TODO consider scenario: There is a CSR that is signed but the certificate itself has expired or was revoked
// Also, at the moment we assume that once the CSR is approved it cannot be rejected.
// What if we approved something by mistake.
- if (nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName) != null) throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name")
-
+ val existingRequestByLegalName = nonRejectedRequest(CertificateSigningRequestEntity::legalName.name, legalName)
+ existingRequestByLegalName?.let {
+ if (certNotRevoked(it)) {
+ throw RequestValidationException(legalName, rejectMessage = "Duplicate legal name")
+ }
+ }
return null
}
+ private fun certNotRevoked(request: CertificateSigningRequestEntity): Boolean {
+ return request.status != RequestStatus.DONE || request.certificateData?.certificateStatus != CertificateStatus.REVOKED
+ }
+
/**
* Retrieve "non-rejected" request which matches provided column and value predicate.
*/
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt
index 1691a52e1b..bf48596fc9 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/common/utils/CrlUtils.kt
@@ -23,14 +23,15 @@ fun createSignedCrl(issuerCertificate: X509Certificate,
endpointUrl: URL,
nextUpdateInterval: Duration,
signer: Signer,
- includeInCrl: List): X509CRL {
+ includeInCrl: List,
+ indirectIssuingPoint: Boolean = false): X509CRL {
val extensionUtils = JcaX509ExtensionUtils()
- val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.issuerX500Principal.encoded), Date())
+ val builder = X509v2CRLBuilder(X500Name.getInstance(issuerCertificate.subjectX500Principal.encoded), Date())
builder.addExtension(Extension.authorityKeyIdentifier, false, extensionUtils.createAuthorityKeyIdentifier(issuerCertificate))
val issuingDistributionPointName = GeneralName(GeneralName.uniformResourceIdentifier, endpointUrl.toString())
- val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), false, false)
+ val issuingDistributionPoint = IssuingDistributionPoint(DistributionPointName(GeneralNames(issuingDistributionPointName)), indirectIssuingPoint, false)
builder.addExtension(Extension.issuingDistributionPoint, true, issuingDistributionPoint)
- builder.setNextUpdate(Date((Instant.now() + nextUpdateInterval).toEpochMilli()))
+ builder.setNextUpdate(Date(Instant.now().toEpochMilli() + nextUpdateInterval.toMillis()))
includeInCrl.forEach {
builder.addCRLEntry(it.certificateSerialNumber, Date(it.modifiedAt.toEpochMilli()), it.reason.ordinal)
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt
index f6d82df265..878696f4ff 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/CrrJiraCient.kt
@@ -14,6 +14,7 @@ import com.atlassian.jira.rest.client.api.JiraRestClient
import com.atlassian.jira.rest.client.api.domain.input.IssueInputBuilder
import com.atlassian.jira.rest.client.api.domain.input.TransitionInput
import com.r3.corda.networkmanage.common.persistence.CertificateRevocationRequestData
+import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.contextLogger
class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClient(restClient, projectCode) {
@@ -31,20 +32,29 @@ class CrrJiraClient(restClient: JiraRestClient, projectCode: String) : JiraClien
"Certificate serial number: ${revocationRequest.certificateSerialNumber}\n" +
"Revocation reason: ${revocationRequest.reason.name}\n" +
"Reporter: ${revocationRequest.reporter}\n" +
- "CSR request ID: ${revocationRequest.certificateSigningRequestId}"
+ "Original CSR request ID: ${revocationRequest.certificateSigningRequestId}"
+
+ val subject = CordaX500Name.build(revocationRequest.certificate.subjectX500Principal)
+ val ticketSummary = if (subject.organisationUnit != null) {
+ "${subject.organisationUnit}, ${subject.organisation}"
+ } else {
+ subject.organisation
+ }
val issue = IssueInputBuilder().setIssueTypeId(taskIssueType.id)
.setProjectKey(projectCode)
.setDescription(ticketDescription)
+ .setSummary(ticketSummary)
.setFieldValue(requestIdField.id, revocationRequest.requestId)
// This will block until the issue is created.
- val issueId = restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key
- val createdIssue = checkNotNull(getIssueById(issueId)) { "Missing the JIRA ticket for the request ID: $issueId" }
+ restClient.issueClient.createIssue(issue.build()).fail { logger.error("Exception when creating JIRA issue.", it) }.claim().key
+ val createdIssue = checkNotNull(getIssueById(revocationRequest.requestId)) { "Missing the JIRA ticket for the request ID: ${revocationRequest.requestId}" }
restClient.issueClient.addAttachment(createdIssue.attachmentsUri, revocationRequest.certificate.encoded.inputStream(), "${revocationRequest.certificateSerialNumber}.cer")
- .fail { CsrJiraClient.logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
+ .fail { logger.error("Error processing request '${createdIssue.key}' : Exception when uploading attachment to JIRA.", it) }.claim()
}
fun updateDoneCertificateRevocationRequest(requestId: String) {
+ logger.debug("Marking JIRA ticket with `request ID` = $requestId as DONE.")
val issue = requireNotNull(getIssueById(requestId)) { "Missing the JIRA ticket for the request ID: $requestId" }
restClient.issueClient.transition(issue, TransitionInput(getTransitionId(DONE_TRANSITION_KEY, issue))).fail { logger.error("Exception when transiting JIRA status.", it) }.claim()
}
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
index 72e78be49e..088d9d14dc 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/DoormanParameters.kt
@@ -11,6 +11,7 @@
package com.r3.corda.networkmanage.doorman
import com.google.common.primitives.Booleans
+import net.corda.core.internal.div
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.seconds
import net.corda.nodeapi.internal.config.OldConfig
@@ -41,7 +42,15 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
// TODO Should be part of a localSigning sub-config
val rootKeystorePassword: String?,
// TODO Should be part of a localSigning sub-config
- val rootPrivateKeyPassword: String?
+ val rootPrivateKeyPassword: String?,
+ // TODO Should be part of a localSigning sub-config
+ val caCrlPath: Path? = null,
+ // TODO Should be part of a localSigning sub-config
+ val caCrlUrl: URL? = null,
+ // TODO Should be part of a localSigning sub-config
+ val emptyCrlPath: Path? = null,
+ // TODO Should be part of a localSigning sub-config
+ val emptyCrlUrl: URL? = null
) {
companion object {
// TODO: Do we really need these defaults?
@@ -53,6 +62,7 @@ data class NetworkManagementServerConfig( // TODO: Move local signing to signing
data class DoormanConfig(val approveAll: Boolean = false,
@OldConfig("jiraConfig")
val jira: JiraConfig? = null,
+ val crlEndpoint: URL? = null,
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
init {
require(Booleans.countTrue(approveAll, jira != null) == 1) {
@@ -65,6 +75,8 @@ data class CertificateRevocationConfig(val approveAll: Boolean = false,
val jira: JiraConfig? = null,
val localSigning: LocalSigning?,
val crlCacheTimeout: Long,
+ val caCrlPath: Path,
+ val emptyCrlPath: Path,
val approveInterval: Long = NetworkManagementServerConfig.DEFAULT_APPROVE_INTERVAL.toMillis()) {
init {
require(Booleans.countTrue(approveAll, jira != null) == 1) {
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
index 72da06ac1f..fecdcb19f5 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/Main.kt
@@ -15,6 +15,7 @@ import com.r3.corda.networkmanage.common.utils.*
import com.r3.corda.networkmanage.doorman.signer.LocalSigner
import net.corda.core.crypto.Crypto
import net.corda.core.internal.exists
+import net.corda.nodeapi.internal.crypto.CertificateAndKeyPair
import net.corda.nodeapi.internal.crypto.X509KeyStore
import net.corda.nodeapi.internal.crypto.X509Utilities
import org.slf4j.LoggerFactory
@@ -36,7 +37,14 @@ fun main(args: Array) {
logger.info("Running in ${cmdLineOptions.mode} mode")
when (cmdLineOptions.mode) {
- Mode.ROOT_KEYGEN -> rootKeyGenMode(cmdLineOptions, config)
+ Mode.ROOT_KEYGEN -> {
+ val emptyCrlPath = requireNotNull(config.emptyCrlPath) { "emptyCrlPath needs to be specified" }
+ val emptyCrlUrl = requireNotNull(config.emptyCrlUrl) { "emptyCrlUrl needs to be specified" }
+ val caCrlPath = requireNotNull(config.caCrlPath) { "caCrlPath needs to be specified" }
+ val caCrlUrl = requireNotNull(config.caCrlUrl) { "caCrlUrl needs to be specified" }
+ val rootCertificateAndKeyPair = rootKeyGenMode(cmdLineOptions, config)
+ createEmptyCrls(rootCertificateAndKeyPair, emptyCrlPath, emptyCrlUrl, caCrlPath, caCrlUrl)
+ }
Mode.CA_KEYGEN -> caKeyGenMode(config)
Mode.DOORMAN -> doormanMode(cmdLineOptions, config)
}
@@ -61,8 +69,8 @@ private fun processKeyStore(config: NetworkManagementServerConfig): Pair, rejectedRequest: List) {
// Reconfirm request status and update jira status
+ logger.debug("Updating JIRA tickets: `approved` = $approvedRequest, `rejected` = $rejectedRequest")
approvedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
.filter { it.status == RequestStatus.DONE }
.forEachWithExceptionLogging(logger) { jiraClient.updateDoneCertificateRevocationRequest(it.requestId) }
-
rejectedRequest.mapNotNull { crrStorage.getRevocationRequest(it.requestId) }
.filter { it.status == RequestStatus.REJECTED }
.forEachWithExceptionLogging(logger) { jiraClient.updateRejectedRequest(it.requestId) }
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt
index f5b98da759..1f2f4931c7 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/doorman/webservice/CertificateRevocationListWebService.kt
@@ -17,6 +17,8 @@ import javax.ws.rs.core.Response.status
@Path(CRL_PATH)
class CertificateRevocationListWebService(private val revocationListStorage: CertificateRevocationListStorage,
+ private val caCrlBytes: ByteArray,
+ private val emptyCrlBytes: ByteArray,
cacheTimeout: Duration) {
companion object {
private val logger = contextLogger()
@@ -24,6 +26,7 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer
const val CRL_DATA_TYPE = "application/pkcs7-crl"
const val DOORMAN = "doorman"
const val ROOT = "root"
+ const val EMPTY = "empty"
}
private val crlCache: LoadingCache = Caffeine.newBuilder()
@@ -43,7 +46,14 @@ class CertificateRevocationListWebService(private val revocationListStorage: Cer
@Path(ROOT)
@Produces(CRL_DATA_TYPE)
fun getRootRevocationList(): Response {
- return getCrlResponse(CrlIssuer.ROOT)
+ return ok(caCrlBytes).build()
+ }
+
+ @GET
+ @Path(EMPTY)
+ @Produces(CRL_DATA_TYPE)
+ fun getEmptyRevocationList(): Response {
+ return ok(emptyCrlBytes).build()
}
private fun getCrlResponse(issuer: CrlIssuer): Response {
diff --git a/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt b/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt
index 6ffba6acf6..75beedbb5a 100644
--- a/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt
+++ b/network-management/src/main/kotlin/com/r3/corda/networkmanage/tools/crr/submission/Main.kt
@@ -29,7 +29,7 @@ fun submit(url: URL, inputReader: InputReader = ConsoleInputReader()) {
val csrRequestId = inputReader.getOptionalInput("certificate signing request ID")
val legalName = inputReader.getOptionalInput("node X.500 legal name")?.let { CordaX500Name.parse(it) }
CertificateRevocationRequest.validateOptional(certificateSerialNumber, csrRequestId, legalName)
- val reason = inputReader.getRequiredInput("revocation reason").let { CRLReason.valueOf(it) }
+ val reason = getReason(inputReader)
val reporter = inputReader.getRequiredInput("reporter of the revocation request")
val request = CertificateRevocationRequest(certificateSerialNumber, csrRequestId, legalName, reason, reporter)
logger.debug("POST to $url request: $request")
@@ -53,4 +53,29 @@ private fun InputReader.getRequiredInput(attributeName: String): String {
} else {
line
}
+}
+
+private enum class SupportedCrlReasons {
+ UNSPECIFIED,
+ KEY_COMPROMISE,
+ CA_COMPROMISE,
+ AFFILIATION_CHANGED,
+ SUPERSEDED,
+ CESSATION_OF_OPERATION,
+ PRIVILEGE_WITHDRAWN
+}
+
+private fun getReason(inputReader: InputReader): CRLReason {
+ while (true) {
+ SupportedCrlReasons.values().forEachIndexed { index, value ->
+ println("${index + 1}. $value")
+ }
+ print("Selected the reason for the revocation:")
+ val input = inputReader.readLine()!!.toInt()
+ if (input < 1 || input > SupportedCrlReasons.values().size) {
+ println("Incorrect selection. Try again.")
+ } else {
+ return CRLReason.valueOf(SupportedCrlReasons.values()[input -1 ].name)
+ }
+ }
}
\ No newline at end of file
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt
new file mode 100644
index 0000000000..be614852ca
--- /dev/null
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/CertificateRevocationListNodeTests.kt
@@ -0,0 +1,108 @@
+package com.r3.corda.networkmanage
+
+import net.corda.core.toFuture
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.P2P_PREFIX
+import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.PEER_USER
+import net.corda.nodeapi.internal.crypto.X509KeyStore
+import net.corda.nodeapi.internal.protonwrapper.messages.MessageStatus
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPClient
+import net.corda.nodeapi.internal.protonwrapper.netty.AMQPServer
+import net.corda.testing.core.ALICE_NAME
+import net.corda.testing.core.BOB_NAME
+import net.corda.testing.core.CHARLIE_NAME
+import net.corda.testing.core.freePort
+import org.bouncycastle.jce.provider.BouncyCastleProvider
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TemporaryFolder
+import java.nio.file.Path
+import java.nio.file.Paths
+import java.security.Security
+import kotlin.test.assertEquals
+
+/**
+ * This test is to perform manual testing of the SSL connection using local key stores. It aims to assess the
+ * correct behaviour of the SSL connection between 2 nodes with respect to the CRL validation.
+ * In order to debug the certificate path validation please use the following JVM parameters when running the test:
+ * -Djavax.net.debug=ssl,handshake -Djava.security.debug=certpath
+ */
+@Ignore
+class CertificateRevocationListNodeTests {
+ @Rule
+ @JvmField
+ val temporaryFolder = TemporaryFolder()
+
+ private val serverPort = freePort()
+
+ private val serverSslKeyStore: Path = Paths.get("/certificatesServer/sslkeystore.jks")
+ private val clientSslKeyStore: Path = Paths.get("/certificatesClient/sslkeystore.jks")
+ private val serverTrustStore: Path = Paths.get("/certificatesServer/truststore.jks")
+ private val clientTrustStore: Path = Paths.get("/certificatesClient/truststore.jks")
+
+ @Before
+ fun setUp() {
+ Security.addProvider(BouncyCastleProvider())
+ }
+
+ @Test
+ fun `Simple AMPQ Client to Server connection works`() {
+ val amqpServer = createServer(serverPort)
+ amqpServer.use {
+ amqpServer.start()
+ val receiveSubs = amqpServer.onReceive.subscribe {
+ assertEquals(BOB_NAME.toString(), it.sourceLegalName)
+ assertEquals(P2P_PREFIX + "Test", it.topic)
+ assertEquals("Test", String(it.payload))
+ it.complete(true)
+ }
+ val amqpClient = createClient(serverPort)
+ amqpClient.use {
+ val serverConnected = amqpServer.onConnection.toFuture()
+ val clientConnected = amqpClient.onConnection.toFuture()
+ amqpClient.start()
+ val serverConnect = serverConnected.get()
+ assertEquals(true, serverConnect.connected)
+ val clientConnect = clientConnected.get()
+ assertEquals(true, clientConnect.connected)
+ val msg = amqpClient.createMessage("Test".toByteArray(),
+ P2P_PREFIX + "Test",
+ ALICE_NAME.toString(),
+ emptyMap())
+ amqpClient.write(msg)
+ assertEquals(MessageStatus.Acknowledged, msg.onComplete.get())
+ receiveSubs.unsubscribe()
+ }
+ }
+ }
+
+ private fun createClient(targetPort: Int): AMQPClient {
+ val tS = X509KeyStore.fromFile(clientTrustStore, "trustpass").internal
+ val sslS = X509KeyStore.fromFile(clientSslKeyStore, "cordacadevpass").internal
+ return AMQPClient(
+ listOf(NetworkHostAndPort("localhost", targetPort)),
+ setOf(ALICE_NAME, CHARLIE_NAME),
+ PEER_USER,
+ PEER_USER,
+ sslS,
+ "cordacadevpass",
+ tS,
+ false)
+ }
+
+ private fun createServer(port: Int): AMQPServer {
+ val tS = X509KeyStore.fromFile(serverTrustStore, "trustpass").internal
+ val sslS = X509KeyStore.fromFile(serverSslKeyStore, "cordacadevpass").internal
+ return AMQPServer(
+ "0.0.0.0",
+ port,
+ PEER_USER,
+ PEER_USER,
+ sslS,
+ "cordacadevpass",
+ tS,
+ false)
+ }
+}
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt
index f0200491c9..7b8bf81b87 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationListStorageTest.kt
@@ -57,6 +57,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
certificateSerialNumber = certificate.serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(requestId)
crrStorage.approveRevocationRequest(requestId, "Approver")
val revocationRequest = crrStorage.getRevocationRequest(requestId)!!
val crl = createDummyCertificateRevocationList(listOf(revocationRequest.certificateSerialNumber))
@@ -80,6 +81,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank A").serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(done)
crrStorage.approveRevocationRequest(done, "Approver")
val doneRevocationRequest = crrStorage.getRevocationRequest(done)!!
@@ -95,6 +97,7 @@ class PersistentCertificateRevocationListStorageTest : TestBase() {
certificateSerialNumber = createNodeCertificate(csrStorage, legalName = "Bank C").serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(approved)
crrStorage.approveRevocationRequest(approved, "Approver")
val approvedRevocationRequest = crrStorage.getRevocationRequest(approved)!!
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt
index bdeb2d604f..024799930e 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/common/persistence/PersistentCertificateRevocationRequestStorageTest.kt
@@ -85,6 +85,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
certificateSerialNumber = createNodeCertificate(csrStorage, "LegalName" + it.toString()).serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(requestId)
crrStorage.approveRevocationRequest(requestId, "Approver")
}
@@ -117,6 +118,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
certificateSerialNumber = certificate.serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(requestId)
// when
crrStorage.approveRevocationRequest(requestId, "Approver")
@@ -135,6 +137,7 @@ class PersistentCertificateRevocationRequestStorageTest : TestBase() {
certificateSerialNumber = certificate.serialNumber,
reason = REVOCATION_REASON,
reporter = REPORTER))
+ crrStorage.markRequestTicketCreated(requestId)
// when
crrStorage.rejectRevocationRequest(requestId, "Rejector", "No reason")
diff --git a/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt b/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt
index f3eb5301f6..793cf4b0ef 100644
--- a/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt
+++ b/network-management/src/test/kotlin/com/r3/corda/networkmanage/tools/crr/submission/CertificateRevocationRequestSubmissionToolTest.kt
@@ -65,7 +65,7 @@ class CertificateRevocationRequestSubmissionToolTest {
givenUserConsoleSequentialInputOnReadLine(request.certificateSerialNumber.toString(),
request.csrRequestId!!,
request.legalName.toString(),
- request.reason.name,
+ "${request.reason.ordinal + 1}",
request.reporter)
val requestId = SecureHash.randomSHA256().toString()
diff --git a/node/build.gradle b/node/build.gradle
index 7800ff24c5..a51b525344 100644
--- a/node/build.gradle
+++ b/node/build.gradle
@@ -71,6 +71,8 @@ dependencies {
compile project(":confidential-identities")
compile project(':client:rpc')
compile project(':tools:shell')
+ runtime project(':launcher')
+
compile "net.corda.plugins:cordform-common:$gradle_plugins_version"
// Log4J: logging framework (with SLF4J bindings)
diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle
index aab294cda1..e4e8984eb3 100644
--- a/node/capsule/build.gradle
+++ b/node/capsule/build.gradle
@@ -28,6 +28,11 @@ dependencies {
capsuleRuntime "com.typesafe:config:$typesafe_config_version"
}
+ext {
+ quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
+ applicationClass = 'net.corda.node.Corda'
+}
+
// Force the Caplet to target Java 6. This ensures that running 'java -jar corda.jar' on any Java 6 VM upwards
// will get as far as the Capsule version checks, meaning that if your JVM is too old, you will at least get
// a sensible error message telling you what to do rather than a bytecode version exception that doesn't.
@@ -37,8 +42,8 @@ sourceCompatibility = 1.6
targetCompatibility = 1.6
task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
- applicationClass 'net.corda.node.Corda'
archiveName "corda-r3-${corda_release_version}.jar"
+ applicationClass 'net.corda.node.Corda'
applicationSource = files(
project(':node').configurations.runtime,
project(':node').jar,
@@ -54,7 +59,6 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) {
applicationVersion = corda_release_version
// See experimental/quasar-hook/README.md for how to generate.
- def quasarExcludeExpression = "x(antlr**;bftsmart**;ch**;co.paralleluniverse**;com.codahale**;com.esotericsoftware**;com.fasterxml**;com.google**;com.ibm**;com.intellij**;com.jcabi**;com.nhaarman**;com.opengamma**;com.typesafe**;com.zaxxer**;de.javakaffee**;groovy**;groovyjarjarantlr**;groovyjarjarasm**;io.atomix**;io.github**;io.netty**;jdk**;joptsimple**;junit**;kotlin**;net.bytebuddy**;net.i2p**;org.apache**;org.assertj**;org.bouncycastle**;org.codehaus**;org.crsh**;org.dom4j**;org.fusesource**;org.h2**;org.hamcrest**;org.hibernate**;org.jboss**;org.jcp**;org.joda**;org.junit**;org.mockito**;org.objectweb**;org.objenesis**;org.slf4j**;org.w3c**;org.xml**;org.yaml**;reflectasm**;rx**;org.jolokia**)"
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"]
systemProperties['visualvm.display.name'] = 'CordaEnterprise'
minJavaVersion = '1.8.0'
diff --git a/node/dist/README.md b/node/dist/README.md
new file mode 100644
index 0000000000..cea059c4d9
--- /dev/null
+++ b/node/dist/README.md
@@ -0,0 +1,5 @@
+This project adds `buildCordaTarball` task to Gradle. It prepares distributable tarball with JRE built-in, using ``javapackager``
+
+For now, it packs the whatever JRE is available in the system, but this will get standarised over time.
+
+It requires ``javapackager`` to be available in the path.
\ No newline at end of file
diff --git a/node/dist/build.gradle b/node/dist/build.gradle
new file mode 100644
index 0000000000..920e5c3e00
--- /dev/null
+++ b/node/dist/build.gradle
@@ -0,0 +1,159 @@
+description 'Package Node as stand-alone application'
+
+evaluationDependsOn(":node")
+evaluationDependsOn(":docs")
+evaluationDependsOn(":launcher")
+
+ext {
+ outputDir = "$buildDir/corda"
+}
+
+def tmpDir = "${buildDir}/tmp/"
+
+configurations {
+ launcherClasspath
+}
+
+// Define location of predefined startup scripts, license and README files
+sourceSets {
+ binFiles {
+ resources {
+ srcDir file('src/main/resources/bin')
+ }
+ }
+ readmeFiles {
+ resources {
+ srcDir file('src/main/resources/readme')
+ }
+ }
+}
+
+// Runtime dependencies of launcher
+dependencies {
+ compile project(':node')
+ launcherClasspath project(':launcher')
+ launcherClasspath "org.slf4j:jul-to-slf4j:$slf4j_version"
+ launcherClasspath "org.apache.logging.log4j:log4j-slf4j-impl:${log4j_version}"
+ launcherClasspath "org.apache.logging.log4j:log4j-web:${log4j_version}"
+
+ // Required by JVM agents:
+ launcherClasspath "com.google.guava:guava:$guava_version"
+ launcherClasspath "de.javakaffee:kryo-serializers:0.41"
+}
+
+task copyLauncherLibs(type: Copy, dependsOn: [project(':launcher').jar]) {
+ from configurations.launcherClasspath
+ into "$buildDir/tmp/launcher-lib"
+}
+
+// The launcher building is done as it depends on application-specific settings which
+// cannot be overridden later
+task buildLauncher(type: Exec, dependsOn: [copyLauncherLibs]) {
+ description 'Build Launcher executable'
+
+ def isLinux = System.properties['os.name'].toLowerCase().contains('linux')
+ def isMac = System.properties['os.name'].toLowerCase().contains('mac')
+
+ if (!isLinux && !isMac)
+ throw new GradleException("Preparing distribution package is currently only supported on Linux/Mac")
+
+ def relativeDir
+ if (isLinux)
+ relativeDir = "launcher"
+ else
+ relativeDir = "launcher.app/Contents"
+
+ def extraArgs = [
+ "-BjvmOptions=-javaagent:../../lib/quasar-core-${quasar_version}-jdk8.jar=${project(':node:capsule').quasarExcludeExpression}",
+ '-BuserJvmOptions=-Xmx=4g',
+ '-BuserJvmOptions=-XX\\:=+UseG1GC',
+ "-BjvmProperties=java.system.class.loader=${project(':launcher').loaderClassName}"
+ ]
+
+ ext {
+ launcherBinDir = "${tmpDir}/bundles/$relativeDir"
+ }
+
+ workingDir project.projectDir
+
+ doFirst {
+ def launcherLib = copyLauncherLibs.destinationDir
+ def srcfiles = []
+ def launcherClasspath = []
+
+ fileTree(launcherLib).forEach({ file ->
+ srcfiles.add("-srcfiles")
+ srcfiles.add(file.name)
+ launcherClasspath.add(file.name)
+ })
+
+ commandLine = [
+ 'javapackager',
+ '-deploy',
+ '-nosign',
+ '-native', 'image',
+ '-outdir', "$tmpDir",
+ '-outfile', 'launcher',
+ '-name', 'launcher',
+ "-BmainJar=${project(':launcher').jar.archiveName}",
+ "-Bclasspath=${launcherClasspath.join(":")}",
+ '-appclass', "${project(':launcher').launcherClassName}",
+ '-srcdir', "$launcherLib"
+ ] + srcfiles + extraArgs
+ }
+
+ // Add configuration for running Node application
+ doLast {
+ def nodeClasspath = []
+ def libRelPath = "../lib/"
+
+ project(':node').configurations.runtime.forEach({ file ->
+ nodeClasspath.add(file.getName())
+ })
+
+ nodeClasspath.add(project(':node').jar.archivePath.getName())
+
+ new File("${launcherBinDir}/runtime.properties").text = [
+ "libpath=${libRelPath}",
+ "classpath=${nodeClasspath.join(':')}",
+ "plugins=./drivers:./cordapps"].join("\n")
+ }
+}
+
+task buildNode(type: Copy, dependsOn: [buildLauncher, project(':docs').tasks['makeDocs'], project(':node').tasks['jar']]) {
+ description 'Build stand-alone Corda Node distribution'
+
+ into(outputDir)
+
+ from(buildLauncher.launcherBinDir) {
+ into("launcher")
+ }
+
+ from(project(':node').configurations.runtime) {
+ into("lib")
+ }
+
+ from(project(':node').jar.archivePath) {
+ into("lib")
+ }
+
+ from(sourceSets.binFiles.resources) {
+ into("bin")
+ }
+
+ from(sourceSets.readmeFiles.resources) {
+ into(".")
+ }
+
+ from(project(':docs').buildDir) {
+ into("docs")
+ }
+
+ doLast {
+ new File("${outputDir}/cordapps").mkdirs()
+ new File("${outputDir}/drivers").mkdirs()
+ println ("Stand-alone Corda Node application available at:")
+ println ("${outputDir}")
+ }
+}
+
diff --git a/node/dist/src/main/resources/bin/corda b/node/dist/src/main/resources/bin/corda
new file mode 100755
index 0000000000..ad5ce399d7
--- /dev/null
+++ b/node/dist/src/main/resources/bin/corda
@@ -0,0 +1,20 @@
+#!/bin/sh
+# ------------------------
+# Corda startup script
+# -------------------------
+
+MAINCLASSNAME="net.corda.node.Corda"
+READLINK=`which readlink`
+
+# Locate this script and relative launcher executable
+SCRIPT_LOCATION=$0
+if [ -x "$READLINK" ]; then
+ while [ -L "$SCRIPT_LOCATION" ]; do
+ SCRIPT_LOCATION=`"$READLINK" -e "$SCRIPT_LOCATION"`
+ done
+fi
+SCRIPT_DIR=`dirname "$SCRIPT_LOCATION"`
+LAUNCHER_LOCATION="$SCRIPT_DIR/../launcher/launcher"
+
+# Run Corda
+CORDA_LAUNCHER_CWD="`pwd`" ${LAUNCHER_LOCATION} ${MAINCLASSNAME} "$@"
diff --git a/node/dist/src/main/resources/readme/README b/node/dist/src/main/resources/readme/README
new file mode 100644
index 0000000000..70ebbc74fd
--- /dev/null
+++ b/node/dist/src/main/resources/readme/README
@@ -0,0 +1,9 @@
+Welcome to Corda Enterprise!
+
+This is a distributon package containing the Java Runtime Environment for convenience.
+
+To start a node, please edit supplied node.conf file so it contains appropriate data for your organization. More - https://docs.corda.net/corda-configuration-file.html
+
+Your CordApps should be placed in cordapps directory, from which they will be loaded automatically.
+
+Linux/Mac: main executable file is ./bin/corda
diff --git a/node/dist/src/main/resources/readme/example-node.conf b/node/dist/src/main/resources/readme/example-node.conf
new file mode 100644
index 0000000000..15d45001d7
--- /dev/null
+++ b/node/dist/src/main/resources/readme/example-node.conf
@@ -0,0 +1,61 @@
+p2pAddress="localhost:10005"
+myLegalName="O=Bank A,L=London,C=GB"
+emailAddress = "admin@company.com"
+keyStorePassword = "cordacadevpass"
+trustStorePassword = "trustpass"
+dataSourceProperties = {
+ dataSourceClassName = org.h2.jdbcx.JdbcDataSource
+ dataSource.url = "jdbc:h2:file:"${baseDirectory}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
+ dataSource.user = sa
+ dataSource.password = ""
+}
+database = {
+ transactionIsolationLevel = "REPEATABLE_READ"
+ exportHibernateJMXStatistics = "false"
+}
+devMode = true
+h2port = 0
+useTestClock = false
+verifierType = InMemory
+rpcSettings = {
+ useSsl = false
+ standAloneBroker = false
+ address="localhost:10007"
+ adminAddress="localhost:10008"
+}
+security {
+ authService {
+ dataSource {
+ type=INMEMORY
+ users=[
+ {
+ password=default
+ permissions=[
+ ALL
+ ]
+ username=default
+ }
+ ]
+ }
+ }
+}
+enterpriseConfiguration = {
+ mutualExclusionConfiguration = {
+ on = false
+ machineName = ""
+ updateInterval = 20000
+ waitInterval = 40000
+ }
+ tuning = {
+ flowThreadPoolSize = 1
+ rpcThreadPoolSize = 4
+ maximumMessagingBatchSize = 256
+ p2pConfirmationWindowSize = 1048576
+ brokerConnectionTtlCheckIntervalMs = 20
+ stateMachine = {
+ eventQueueSize = 16
+ sessionDeliverPersistenceStrategy = "OnNextCommit"
+ }
+ }
+ useMultiThreadedSMM = true
+}
diff --git a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
index 4e885beea9..babd44a57e 100644
--- a/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
+++ b/node/src/main/kotlin/net/corda/node/NodeArgsParser.kt
@@ -68,7 +68,12 @@ class NodeArgsParser : AbstractArgsParser() {
require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) {
"${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together"
}
- val baseDirectory = optionSet.valueOf(baseDirectoryArg).normalize().toAbsolutePath()
+ // Workaround for javapackager polluting cwd: restore it from system property set by launcher.
+ val baseDirectory = System.getProperty("corda.launcher.cwd")?.let { Paths.get(it) }
+ ?: optionSet.valueOf(baseDirectoryArg)
+ .normalize()
+ .toAbsolutePath()
+
val configFile = baseDirectory / optionSet.valueOf(configFileArg)
val loggingLevel = optionSet.valueOf(loggerLevel)
val logToConsole = optionSet.has(logToConsoleArg)
diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
index 0aaf718546..e3184bb355 100644
--- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
+++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt
@@ -29,6 +29,7 @@ import net.corda.nodeapi.internal.persistence.DatabaseConfig
import net.corda.tools.shell.SSHDConfiguration
import org.bouncycastle.asn1.x500.X500Name
import org.slf4j.Logger
+import sun.security.x509.X500Name
import java.net.URL
import java.nio.file.Path
import java.time.Duration
diff --git a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
index e6c76aa71f..cfaf33b036 100644
--- a/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
+++ b/node/src/test/kotlin/net/corda/node/services/config/NodeConfigurationImplTest.kt
@@ -24,6 +24,7 @@ import net.corda.tools.shell.SSHDConfiguration
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Test
+import java.net.URL
import java.net.URI
import java.net.URL
import java.nio.file.Paths
diff --git a/settings.gradle b/settings.gradle
index 82ada2052d..9b83f51f66 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -56,6 +56,7 @@ include 'tools:explorer'
include 'tools:explorer:capsule'
include 'tools:demobench'
include 'tools:loadtest'
+include 'tools:notarytest'
include 'tools:graphs'
include 'tools:bootstrapper'
include 'tools:dbmigration'
@@ -82,3 +83,5 @@ project(':hsm-tool').with {
name = 'sgx-hsm-tool'
projectDir = file("$settingsDir/sgx-jvm/hsm-tool")
}
+include 'launcher'
+include 'node:dist'
diff --git a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt
index 710d246cd5..fb819be221 100644
--- a/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt
+++ b/testing/node-driver/src/main/kotlin/net/corda/testing/node/internal/InternalTestUtils.kt
@@ -151,9 +151,9 @@ internal interface InternalMockMessagingService : MessagingService {
* @param fallBackConfigSupplier Returns [Config] with dataSourceProperties, invoked with [nodeName] and [nodeNameExtension] parameters.
* Defaults to configuration of in-memory H2 instance.
*/
-fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().toString(),
+fun makeTestDataSourceProperties(nodeName: String? = SecureHash.randomSHA256().toString(),
nodeNameExtension: String? = null,
- configSupplier: (String, String?) -> Config = ::databaseProviderDataSourceConfig,
+ configSupplier: (String?, String?) -> Config = ::databaseProviderDataSourceConfig,
fallBackConfigSupplier: (String?, String?) -> Config = ::inMemoryH2DataSourceConfig): Properties {
val config = configSupplier(nodeName, nodeNameExtension)
.withFallback(fallBackConfigSupplier(nodeName, nodeNameExtension))
@@ -172,9 +172,11 @@ fun makeTestDataSourceProperties(nodeName: String = SecureHash.randomSHA256().to
* Make properties appropriate for creating a Database for unit tests.
*
* @param nodeName Reflects the "instance" of the in-memory database or database username/schema.
+ * @param configSupplier Returns [Config] with databaseProperties, invoked with [nodeName] parameter.
*/
-fun makeTestDatabaseProperties(nodeName: String? = null): DatabaseConfig {
- val config = databaseProviderDataSourceConfig(nodeName)
+fun makeTestDatabaseProperties(nodeName: String? = null,
+ configSupplier: (String?, String?) -> Config = ::databaseProviderDataSourceConfig): DatabaseConfig {
+ val config = configSupplier(nodeName, null)
val transactionIsolationLevel = if (config.hasPath(DatabaseConstants.TRANSACTION_ISOLATION_LEVEL))
TransactionIsolationLevel.valueOf(config.getString(DatabaseConstants.TRANSACTION_ISOLATION_LEVEL))
else TransactionIsolationLevel.READ_COMMITTED
diff --git a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
index 4c140b744e..87baa6449f 100644
--- a/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
+++ b/testing/test-utils/src/main/kotlin/net/corda/testing/internal/InternalTestUtils.kt
@@ -10,7 +10,6 @@
package net.corda.testing.internal
-import com.nhaarman.mockito_kotlin.doAnswer
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.Crypto.generateKeyPair
import net.corda.core.identity.CordaX500Name
diff --git a/tools/notarytest/README.md b/tools/notarytest/README.md
new file mode 100644
index 0000000000..d369668d5e
--- /dev/null
+++ b/tools/notarytest/README.md
@@ -0,0 +1,7 @@
+# Notary test tool
+
+Provides building blocks for experimenting and load testing new notary implementations:
+* Deploying a custom notary service locally.
+* Invoking a load generating flow on a notary node.
+
+No scripts for deploying nodes to a remote test cluster are provided.
\ No newline at end of file
diff --git a/tools/notarytest/build.gradle b/tools/notarytest/build.gradle
new file mode 100644
index 0000000000..98191d76bd
--- /dev/null
+++ b/tools/notarytest/build.gradle
@@ -0,0 +1,61 @@
+import net.corda.plugins.Cordform
+
+apply plugin: 'java'
+apply plugin: 'kotlin'
+apply plugin: 'idea'
+apply plugin: 'net.corda.plugins.quasar-utils'
+apply plugin: 'net.corda.plugins.publish-utils'
+apply plugin: 'net.corda.plugins.cordapp'
+apply plugin: 'net.corda.plugins.cordformation'
+apply plugin: 'maven-publish'
+
+dependencies {
+ compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
+ testCompile "junit:junit:$junit_version"
+
+ // Corda integration dependencies
+ cordaCompile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
+ cordaCompile project(':core')
+ cordaCompile project(':client:rpc')
+ cordaCompile project(':node-driver')
+ compile project(':client:mock')
+ compile group: 'mysql', name: 'mysql-connector-java', version: '6.0.6'
+ compile group: 'io.dropwizard.metrics', name: 'metrics-graphite', version: '3.2.5'
+
+}
+
+idea {
+ module {
+ downloadJavadoc = true // defaults to false
+ downloadSources = true
+ }
+}
+
+publishing {
+ publications {
+ jarAndSources(MavenPublication) {
+ from components.java
+ artifactId 'notarytest'
+
+ artifact sourceJar
+ artifact javadocJar
+ }
+ }
+}
+
+task deployJDBC(type: Cordform, dependsOn: 'jar') {
+ definitionClass = 'net.corda.notarytest.JDBCNotaryCordform'
+}
+
+task runTest(type: JavaExec) {
+ classpath = sourceSets.main.runtimeClasspath
+ main = 'net.corda.notarytest.MainKt'
+}
+
+jar {
+ manifest {
+ attributes(
+ 'Automatic-Module-Name': 'net.corda.notarytest'
+ )
+ }
+}
diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt
new file mode 100644
index 0000000000..47489a1cc3
--- /dev/null
+++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/JDBCNotaryCordform.kt
@@ -0,0 +1,82 @@
+package net.corda.notarytest
+
+import net.corda.cordform.CordformContext
+import net.corda.cordform.CordformDefinition
+import net.corda.cordform.CordformNode
+import net.corda.core.identity.CordaX500Name
+import net.corda.node.services.Permissions
+import net.corda.node.services.config.NotaryConfig
+import net.corda.nodeapi.internal.DevIdentityGenerator
+import net.corda.testing.node.User
+import net.corda.testing.node.internal.demorun.*
+
+fun main(args: Array) = JDBCNotaryCordform().nodeRunner().deployAndRunNodes()
+
+internal val notaryDemoUser = User("demou", "demop", setOf(Permissions.all()))
+
+class JDBCNotaryCordform : CordformDefinition() {
+ private val clusterName = CordaX500Name("Mysql Notary", "Zurich", "CH")
+ private val notaryNames = createNotaryNames(3)
+
+ private fun createNotaryNames(clusterSize: Int) = (0 until clusterSize).map {
+ CordaX500Name("Notary Service $it", "Zurich", "CH")
+ }
+
+ init {
+ fun notaryNode(index: Int, configure: CordformNode.() -> Unit) = node {
+ name(notaryNames[index])
+ notary(
+ NotaryConfig(
+ validating = true,
+ custom = true
+ )
+ )
+ extraConfig = mapOf("custom" to
+ mapOf(
+ "mysql" to mapOf(
+ "dataSource" to mapOf(
+ // Update the db address/port accordingly
+ "jdbcUrl" to "jdbc:mysql://localhost:330${6 + index}/corda?rewriteBatchedStatements=true&useSSL=false&failOverReadOnly=false",
+ "username" to "corda",
+ "password" to "awesome",
+ "autoCommit" to "false")
+ ),
+ "graphiteAddress" to "performance-metrics.northeurope.cloudapp.azure.com:2004"
+ )
+ )
+ configure()
+ }
+
+ notaryNode(0) {
+ p2pPort(10009)
+ rpcSettings {
+ address("localhost:10010")
+ adminAddress("localhost:10110")
+ }
+ rpcUsers(notaryDemoUser)
+ }
+ notaryNode(1) {
+ p2pPort(10013)
+ rpcSettings {
+ address("localhost:10014")
+ adminAddress("localhost:10114")
+ }
+ rpcUsers(notaryDemoUser)
+ }
+ notaryNode(2) {
+ p2pPort(10017)
+ rpcSettings {
+ address("localhost:10018")
+ adminAddress("localhost:10118")
+ }
+ rpcUsers(notaryDemoUser)
+ }
+ }
+
+ override fun setup(context: CordformContext) {
+ DevIdentityGenerator.generateDistributedNotarySingularIdentity(
+ notaryNames.map { context.baseDirectory(it.toString()) },
+ clusterName
+ )
+ }
+}
\ No newline at end of file
diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt
new file mode 100644
index 0000000000..b966f6fbd6
--- /dev/null
+++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/Main.kt
@@ -0,0 +1,67 @@
+package net.corda.notarytest
+
+import com.google.common.base.Stopwatch
+import net.corda.client.rpc.CordaRPCClient
+import net.corda.core.messaging.CordaRPCOps
+import net.corda.core.messaging.startFlow
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.notarytest.service.JDBCLoadTestFlow
+import java.io.File
+import java.io.PrintWriter
+import java.time.Instant
+import java.util.concurrent.TimeUnit
+
+/** The number of test flows to run on each notary node */
+const val TEST_RUNS = 1
+/** Total number of transactions to generate and notarise. */
+const val TRANSACTION_COUNT = 10000000
+/** Number of transactions to submit before awaiting completion. */
+const val BATCH_SIZE = 1000
+
+fun main(args: Array) {
+ // Provide a list of notary node addresses to invoke the load generation flow on
+ val addresses = listOf(
+ NetworkHostAndPort("localhost", 10010),
+ NetworkHostAndPort("localhost", 11014),
+ NetworkHostAndPort("localhost", 11018)
+ )
+
+ addresses.parallelStream().forEach {
+ val node = it
+ println("Connecting to the recipient node ($node)")
+
+ CordaRPCClient(it).start(notaryDemoUser.username, notaryDemoUser.password).use {
+ println(it.proxy.nodeInfo())
+ val totalTime = Stopwatch.createStarted()
+ val durations = run(it.proxy, 1)
+ totalTime.stop()
+
+ val totalTx = TEST_RUNS * TRANSACTION_COUNT
+ println("Total duration for $totalTx transactions: ${totalTime.elapsed(TimeUnit.MILLISECONDS)} ms")
+ println("Average tx/s: ${totalTx.toDouble() / totalTime.elapsed(TimeUnit.MILLISECONDS).toDouble() * 1000}")
+
+ // Uncomment to generate a CSV report
+ // printCSV(node, durations, TEST_RUNS, BATCH_SIZE)
+ }
+ }
+}
+
+private fun run(rpc: CordaRPCOps, inputStateCount: Int? = null): List {
+ return (1..TEST_RUNS).map { i ->
+ val timer = Stopwatch.createStarted()
+ val commitDuration = rpc.startFlow(::JDBCLoadTestFlow, TRANSACTION_COUNT, BATCH_SIZE, inputStateCount).returnValue.get()
+ val flowDuration = timer.stop().elapsed(TimeUnit.MILLISECONDS)
+ println("#$i: Duration: $flowDuration ms, commit duration: $commitDuration ms")
+ flowDuration
+ }
+}
+
+private fun printCSV(node: NetworkHostAndPort, durations: List, testRuns: Int, batchSize: Int) {
+ val pw = PrintWriter(File("notarytest-${Instant.now()}-${node.host}${node.port}-${testRuns}x$batchSize.csv"))
+ val sb = StringBuilder()
+ sb.append("$testRuns, $batchSize")
+ sb.append('\n')
+ sb.append(durations.joinToString())
+ pw.write(sb.toString())
+ pw.close()
+}
\ No newline at end of file
diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt
new file mode 100644
index 0000000000..c6d272637c
--- /dev/null
+++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/AsyncLoadTestFlow.kt
@@ -0,0 +1,86 @@
+package net.corda.notarytest.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.google.common.base.Stopwatch
+import net.corda.client.mock.Generator
+import net.corda.core.concurrent.CordaFuture
+import net.corda.core.contracts.StateRef
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.entropyToKeyPair
+import net.corda.core.crypto.generateKeyPair
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.NotarisationRequest
+import net.corda.core.flows.NotarisationRequestSignature
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.concurrent.transpose
+import net.corda.core.internal.notary.AsyncCFTNotaryService
+import net.corda.core.internal.notary.AsyncUniquenessProvider
+import net.corda.core.internal.notary.generateSignature
+import java.math.BigInteger
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+@StartableByRPC
+open class AsyncLoadTestFlow(
+ private val serviceType: Class,
+ private val transactionCount: Int,
+ private val batchSize: Int = 100,
+ /**
+ * Number of input states per transaction.
+ * If *null*, variable sized transactions will be created with median size 4.
+ */
+ private val inputStateCount: Int? = null
+) : FlowLogic() {
+ private val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueOf(it)) }
+ private val publicKeyGeneratorSingle = Generator.pure(generateKeyPair().public)
+ private val partyGenerator: Generator = Generator.int().combine(publicKeyGeneratorSingle) { n, key ->
+ Party(CordaX500Name(organisation = "Party$n", locality = "London", country = "GB"), key)
+ }
+ private val txIdGenerator = Generator.bytes(32).map { SecureHash.sha256(it) }
+ private val stateRefGenerator = txIdGenerator.combine(Generator.intRange(0, 10)) { id, pos -> StateRef(id, pos) }
+
+ @Suspendable
+ override fun call(): Long {
+ var current = 0
+ var totalDuration = 0L
+ while (current < transactionCount) {
+ val batch = Math.min(batchSize, transactionCount - current)
+ totalDuration += runBatch(batch)
+ current += batch
+ }
+ return totalDuration
+ }
+
+ private val random = SplittableRandom()
+
+ private fun runBatch(transactionCount: Int): Long {
+ val stopwatch = Stopwatch.createStarted()
+ val futures = mutableListOf>()
+
+ val service = serviceHub.cordaService(serviceType)
+
+ for (i in 1..batchSize) {
+ val txId: SecureHash = txIdGenerator.generateOrFail(random)
+ val callerParty = partyGenerator.generateOrFail(random)
+ val inputGenerator = if (inputStateCount == null) {
+ Generator.replicatePoisson(4.0, stateRefGenerator, true)
+ } else {
+ Generator.replicate(inputStateCount, stateRefGenerator)
+ }
+ val inputs = inputGenerator.generateOrFail(random)
+ val requestSignature = NotarisationRequest(inputs, txId).generateSignature(serviceHub)
+
+ futures += AsyncCFTNotaryService.CommitOperation(service, inputs, txId, callerParty, requestSignature, null).execute()
+ }
+
+ futures.transpose().get()
+
+ stopwatch.stop()
+ val duration = stopwatch.elapsed(TimeUnit.MILLISECONDS)
+ logger.info("Committed $transactionCount transactions in $duration ms, avg ${duration.toDouble() / transactionCount} ms")
+
+ return duration
+ }
+}
\ No newline at end of file
diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt
new file mode 100644
index 0000000000..b0b78e084e
--- /dev/null
+++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/flows/LoadTestFlow.kt
@@ -0,0 +1,67 @@
+package net.corda.notarytest.flows
+
+import co.paralleluniverse.fibers.Suspendable
+import com.google.common.base.Stopwatch
+import net.corda.client.mock.Generator
+import net.corda.core.contracts.StateRef
+import net.corda.core.crypto.SecureHash
+import net.corda.core.crypto.entropyToKeyPair
+import net.corda.core.crypto.generateKeyPair
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.NotarisationRequest
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.identity.CordaX500Name
+import net.corda.core.identity.Party
+import net.corda.core.internal.notary.TrustedAuthorityNotaryService
+import net.corda.core.internal.notary.generateSignature
+import java.math.BigInteger
+import java.util.*
+import java.util.concurrent.TimeUnit
+
+@StartableByRPC
+open class LoadTestFlow(
+ private val serviceType: Class,
+ private val transactionCount: Int,
+ /**
+ * Number of input states per transaction.
+ * If *null*, variable sized transactions will be created with median size 4.
+ */
+ private val inputStateCount: Int?
+) : FlowLogic() {
+ private val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueOf(it)) }
+ private val publicKeyGenerator = keyPairGenerator.map { it.public }
+
+ private val publicKeyGenerator2 = Generator.pure(generateKeyPair().public)
+ private val partyGenerator: Generator = Generator.int().combine(publicKeyGenerator2) { n, key ->
+ Party(CordaX500Name(organisation = "Party$n", locality = "London", country = "GB"), key)
+ }
+ private val txIdGenerator = Generator.bytes(32).map { SecureHash.sha256(it) }
+ private val stateRefGenerator = Generator.intRange(0, 10).map { StateRef(SecureHash.randomSHA256(), it) }
+
+ @Suspendable
+ override fun call(): Long {
+ val stopwatch = Stopwatch.createStarted()
+ val random = SplittableRandom()
+
+ for (i in 1..transactionCount) {
+ val txId: SecureHash = txIdGenerator.generateOrFail(random)
+ val callerParty = partyGenerator.generateOrFail(random)
+ val inputGenerator = if (inputStateCount == null) {
+ Generator.replicatePoisson(4.0, stateRefGenerator, true)
+ } else {
+ Generator.replicate(inputStateCount, stateRefGenerator)
+ }
+ val inputs = inputGenerator.generateOrFail(random)
+ val localStopwatch = Stopwatch.createStarted()
+ val sig = NotarisationRequest(inputs, txId).generateSignature(serviceHub)
+ serviceHub.cordaService(serviceType).commitInputStates(inputs, txId, callerParty, sig, null)
+ logger.info("Committed a transaction ${txId.toString().take(10)} with ${inputs.size} inputs in ${localStopwatch.stop().elapsed(TimeUnit.MILLISECONDS)} ms")
+ }
+
+ stopwatch.stop()
+ val duration = stopwatch.elapsed(TimeUnit.MILLISECONDS)
+ logger.info("Committed $transactionCount transactions in $duration, avg ${duration.toDouble() / transactionCount} ms")
+
+ return duration
+ }
+}
\ No newline at end of file
diff --git a/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt b/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt
new file mode 100644
index 0000000000..b81ec0863a
--- /dev/null
+++ b/tools/notarytest/src/main/kotlin/net/corda/notarytest/service/JDBCNotaryService.kt
@@ -0,0 +1,73 @@
+package net.corda.notarytest.service
+
+import com.codahale.metrics.MetricFilter
+import com.codahale.metrics.MetricRegistry
+import com.codahale.metrics.graphite.GraphiteReporter
+import com.codahale.metrics.graphite.PickledGraphite
+import net.corda.core.flows.FlowLogic
+import net.corda.core.flows.FlowSession
+import net.corda.core.flows.StartableByRPC
+import net.corda.core.internal.notary.AsyncCFTNotaryService
+import net.corda.core.node.AppServiceHub
+import net.corda.core.node.services.CordaService
+import net.corda.core.utilities.NetworkHostAndPort
+import net.corda.node.services.config.ConfigHelper
+import net.corda.node.services.config.MySQLConfiguration
+import net.corda.node.services.transactions.MySQLUniquenessProvider
+import net.corda.node.services.transactions.NonValidatingNotaryFlow
+import net.corda.nodeapi.internal.config.parseAs
+import net.corda.notarytest.flows.AsyncLoadTestFlow
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.nio.file.Paths
+import java.security.PublicKey
+import java.util.concurrent.TimeUnit
+
+@CordaService
+class JDBCNotaryService(override val services: AppServiceHub, override val notaryIdentityKey: PublicKey) : AsyncCFTNotaryService() {
+ private val appConfig = ConfigHelper.loadConfig(Paths.get(".")).getConfig("custom")
+
+ override val asyncUniquenessProvider: MySQLUniquenessProvider = createUniquenessProvider()
+
+ override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic = NonValidatingNotaryFlow(otherPartySession, this)
+
+ override fun start() {
+ asyncUniquenessProvider.createTable()
+ }
+
+ override fun stop() {
+ asyncUniquenessProvider.stop()
+ }
+
+ private fun createMetricsRegistry(): MetricRegistry {
+ val graphiteAddress = appConfig.getString("graphiteAddress").let { NetworkHostAndPort.parse(it) }
+ val hostName = InetAddress.getLocalHost().hostName.replace(".", "_")
+ val nodeName = services.myInfo.legalIdentities.first().name.organisation
+ .toLowerCase()
+ .replace(" ", "_")
+ .replace(".", "_")
+ val pickledGraphite = PickledGraphite(
+ InetSocketAddress(graphiteAddress.host, graphiteAddress.port)
+ )
+ val metrics = MetricRegistry()
+ GraphiteReporter.forRegistry(metrics)
+ .prefixedWith("corda.$hostName.$nodeName")
+ .convertRatesTo(TimeUnit.SECONDS)
+ .convertDurationsTo(TimeUnit.MILLISECONDS)
+ .filter(MetricFilter.ALL)
+ .build(pickledGraphite)
+ .start(10, TimeUnit.SECONDS)
+ return metrics
+ }
+
+ private fun createUniquenessProvider(): MySQLUniquenessProvider {
+ val mysqlConfig = appConfig.getConfig("mysql").parseAs()
+ return MySQLUniquenessProvider(createMetricsRegistry(), services.clock, mysqlConfig)
+ }
+}
+
+@StartableByRPC
+class JDBCLoadTestFlow(transactionCount: Int,
+ batchSize: Int,
+ inputStateCount: Int?
+) : AsyncLoadTestFlow(JDBCNotaryService::class.java, transactionCount, batchSize, inputStateCount)
\ No newline at end of file