mirror of
https://github.com/corda/corda.git
synced 2025-06-16 14:18:20 +00:00
Merge branch 'master' into tudor_merge_os_24_10
# Conflicts: # core/src/main/kotlin/net/corda/core/internal/JarSignatureCollector.kt # core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt # core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt # core/src/main/kotlin/net/corda/core/utilities/KotlinUtils.kt # core/src/test/kotlin/net/corda/core/contracts/PackageOwnershipVerificationTests.kt # core/src/test/kotlin/net/corda/core/internal/JarSignatureCollectorTest.kt # node/src/main/kotlin/net/corda/node/internal/cordapp/JarScanningCordappLoader.kt # node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentServiceTest.kt # testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TestDSL.kt # testing/test-utils/src/main/kotlin/net/corda/testing/dsl/TransactionDSLInterpreter.kt # testing/test-utils/src/main/kotlin/net/corda/testing/internal/MockCordappProvider.kt
This commit is contained in:
@ -25,7 +25,7 @@ class NodeKeystoreCheckTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `node should throw exception if cert path doesn't chain to the trust root`() {
|
||||
fun `node should throw exception if cert path does not chain to the trust root`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, notarySpecs = emptyList())) {
|
||||
// Create keystores.
|
||||
val keystorePassword = "password"
|
||||
@ -49,9 +49,9 @@ class NodeKeystoreCheckTest {
|
||||
// Self signed root.
|
||||
val badRootKeyPair = Crypto.generateKeyPair()
|
||||
val badRoot = X509Utilities.createSelfSignedCACertificate(X500Principal("O=Bad Root,L=Lodnon,C=GB"), badRootKeyPair)
|
||||
val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)
|
||||
val nodeCA = getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, signingCertStore.entryPassword)
|
||||
val badNodeCACert = X509Utilities.createCertificate(CertificateType.NODE_CA, badRoot, badRootKeyPair, ALICE_NAME.x500Principal, nodeCA.keyPair.public)
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot))
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeCA.keyPair.private, listOf(badNodeCACert, badRoot), signingCertStore.entryPassword)
|
||||
}
|
||||
|
||||
assertThatThrownBy {
|
||||
|
@ -423,17 +423,17 @@ class CertificateRevocationListNodeTests {
|
||||
val signingCertificateStore = first
|
||||
val p2pSslConfiguration = second
|
||||
val nodeKeyStore = signingCertificateStore.get()
|
||||
val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA) }
|
||||
val (nodeCert, nodeKeys) = nodeKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, nodeKeyStore.entryPassword) }
|
||||
val newNodeCert = replaceCrlDistPointCaCertificate(nodeCert, CertificateType.NODE_CA, INTERMEDIATE_CA.keyPair, nodeCaCrlDistPoint)
|
||||
val nodeCertChain = listOf(newNodeCert, INTERMEDIATE_CA.certificate, *nodeKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_CA) }.drop(2).toTypedArray())
|
||||
nodeKeyStore.update {
|
||||
internal.deleteEntry(X509Utilities.CORDA_CLIENT_CA)
|
||||
}
|
||||
nodeKeyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain)
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, nodeKeys.private, nodeCertChain, nodeKeyStore.entryPassword)
|
||||
}
|
||||
val sslKeyStore = p2pSslConfiguration.keyStore.get()
|
||||
val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS) }
|
||||
val (tlsCert, tlsKeys) = sslKeyStore.query { getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, sslKeyStore.entryPassword) }
|
||||
val newTlsCert = replaceCrlDistPointCaCertificate(tlsCert, CertificateType.TLS, nodeKeys, tlsCrlDistPoint, X500Name.getInstance(ROOT_CA.certificate.subjectX500Principal.encoded))
|
||||
val sslCertChain = listOf(newTlsCert, newNodeCert, INTERMEDIATE_CA.certificate, *sslKeyStore.query { getCertificateChain(X509Utilities.CORDA_CLIENT_TLS) }.drop(3).toTypedArray())
|
||||
|
||||
@ -441,7 +441,7 @@ class CertificateRevocationListNodeTests {
|
||||
internal.deleteEntry(X509Utilities.CORDA_CLIENT_TLS)
|
||||
}
|
||||
sslKeyStore.update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain)
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeys.private, sslCertChain, sslKeyStore.entryPassword)
|
||||
}
|
||||
return newNodeCert
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.packageName
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
@ -12,8 +11,8 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.TestCorDapp
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.internal.cordappForClasses
|
||||
import org.junit.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@ -46,23 +45,18 @@ class AsymmetricCorDappsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noSharedCorDappsWithAsymmetricSpecificClasses() {
|
||||
|
||||
fun `no shared cordapps with asymmetric specific classes`() {
|
||||
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = emptySet())) {
|
||||
|
||||
val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java)))).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(TestCorDapp.Factory.create("Szymon CorDapp", "1.0", classes = setOf(Ping::class.java, Pong::class.java)))).getOrThrow()
|
||||
val nodeA = startNode(providedName = ALICE_NAME, additionalCordapps = setOf(cordappForClasses(Ping::class.java))).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForClasses(Ping::class.java, Pong::class.java))).getOrThrow()
|
||||
nodeA.rpc.startFlow(::Ping, nodeB.nodeInfo.singleIdentity(), 1).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sharedCorDappsWithAsymmetricSpecificClasses() {
|
||||
|
||||
val resourceName = "cordapp.properties"
|
||||
val cordappPropertiesResource = this::class.java.getResource(resourceName)
|
||||
val sharedCordapp = TestCorDapp.Factory.create("shared", "1.0", classes = setOf(Ping::class.java)).plusResource("${AsymmetricCorDappsTests::class.java.packageName}.$resourceName", cordappPropertiesResource)
|
||||
val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java))
|
||||
fun `shared cordapps with asymmetric specific classes`() {
|
||||
val sharedCordapp = cordappForClasses(Ping::class.java)
|
||||
val cordappForNodeB = cordappForClasses(Pong::class.java)
|
||||
driver(DriverParameters(startNodesInProcess = false, cordappsForAllNodes = setOf(sharedCordapp))) {
|
||||
|
||||
val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
@ -71,12 +65,9 @@ class AsymmetricCorDappsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun sharedCorDappsWithAsymmetricSpecificClassesInProcess() {
|
||||
|
||||
val resourceName = "cordapp.properties"
|
||||
val cordappPropertiesResource = this::class.java.getResource(resourceName)
|
||||
val sharedCordapp = TestCorDapp.Factory.create("shared", "1.0", classes = setOf(Ping::class.java)).plusResource("${AsymmetricCorDappsTests::class.java.packageName}.$resourceName", cordappPropertiesResource)
|
||||
val cordappForNodeB = TestCorDapp.Factory.create("nodeB_only", "1.0", classes = setOf(Pong::class.java))
|
||||
fun `shared cordapps with asymmetric specific classes in process`() {
|
||||
val sharedCordapp = cordappForClasses(Ping::class.java)
|
||||
val cordappForNodeB = cordappForClasses(Pong::class.java)
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = setOf(sharedCordapp))) {
|
||||
|
||||
val (nodeA, nodeB) = listOf(startNode(providedName = ALICE_NAME), startNode(providedName = BOB_NAME, additionalCordapps = setOf(cordappForNodeB))).transpose().getOrThrow()
|
||||
|
@ -1,27 +1,30 @@
|
||||
package net.corda.node.flows
|
||||
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.list
|
||||
import net.corda.core.internal.moveTo
|
||||
import net.corda.core.internal.readLines
|
||||
import net.corda.core.messaging.startTrackedFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.CheckpointIncompatibleException
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.services.Permissions.Companion.invokeRpc
|
||||
import net.corda.node.services.Permissions.Companion.startFlow
|
||||
import net.corda.testMessage.Message
|
||||
import net.corda.testMessage.MessageState
|
||||
import net.corda.testMessage.*
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.TestCorDapp
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import net.corda.testing.node.TestCordapp
|
||||
import net.corda.testing.node.internal.ListenProcessDeathException
|
||||
import net.corda.testing.node.internal.TestCordappDirectories
|
||||
import net.corda.testing.node.internal.cordappForClasses
|
||||
import net.test.cordapp.v1.Record
|
||||
import net.test.cordapp.v1.SendMessageFlow
|
||||
import org.junit.Test
|
||||
import java.nio.file.StandardCopyOption.REPLACE_EXISTING
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
@ -30,125 +33,110 @@ import kotlin.test.assertNotNull
|
||||
class FlowCheckpointVersionNodeStartupCheckTest {
|
||||
companion object {
|
||||
val message = Message("Hello world!")
|
||||
val classes = setOf(net.corda.testMessage.MessageState::class.java,
|
||||
net.corda.testMessage.MessageContract::class.java,
|
||||
net.test.cordapp.v1.SendMessageFlow::class.java,
|
||||
net.corda.testMessage.MessageSchema::class.java,
|
||||
net.corda.testMessage.MessageSchemaV1::class.java,
|
||||
net.test.cordapp.v1.Record::class.java)
|
||||
val user = User("mark", "dadada", setOf(startFlow<SendMessageFlow>(), invokeRpc("vaultQuery"), invokeRpc("vaultTrack")))
|
||||
val defaultCordapp = cordappForClasses(
|
||||
MessageState::class.java,
|
||||
MessageContract::class.java,
|
||||
SendMessageFlow::class.java,
|
||||
MessageSchema::class.java,
|
||||
MessageSchemaV1::class.java,
|
||||
Record::class.java
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restart node successfully with suspended flow`() {
|
||||
|
||||
val cordapps = setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes))
|
||||
|
||||
return driver(DriverParameters(isDebug = true, startNodesInProcess = false, inMemoryDB = false, cordappsForAllNodes = cordapps)) {
|
||||
{
|
||||
val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME).getOrThrow()
|
||||
val bob = startNode(rpcUsers = listOf(user), providedName = BOB_NAME).getOrThrow()
|
||||
alice.stop()
|
||||
CordaRPCClient(bob.rpcAddress).start(user.username, user.password).use {
|
||||
val flowTracker = it.proxy.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress
|
||||
//wait until Bob progresses as far as possible because alice node is off
|
||||
flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single()
|
||||
}
|
||||
bob.stop()
|
||||
}()
|
||||
val result = {
|
||||
//Bob will resume the flow
|
||||
val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to false)).getOrThrow()
|
||||
CordaRPCClient(alice.rpcAddress).start(user.username, user.password).use {
|
||||
val page = it.proxy.vaultTrack(MessageState::class.java)
|
||||
if (page.snapshot.states.isNotEmpty()) {
|
||||
page.snapshot.states.first()
|
||||
} else {
|
||||
val r = page.updates.timeout(5, TimeUnit.SECONDS).take(1).toBlocking().single()
|
||||
if (r.consumed.isNotEmpty()) r.consumed.first() else r.produced.first()
|
||||
}
|
||||
}
|
||||
}()
|
||||
return driver(parametersForRestartingNodes(listOf(defaultCordapp))) {
|
||||
createSuspendedFlowInBob(cordapps = emptySet())
|
||||
// Bob will resume the flow
|
||||
val alice = startNode(providedName = ALICE_NAME).getOrThrow()
|
||||
startNode(providedName = BOB_NAME).getOrThrow()
|
||||
val page = alice.rpc.vaultTrack(MessageState::class.java)
|
||||
val result = if (page.snapshot.states.isNotEmpty()) {
|
||||
page.snapshot.states.first()
|
||||
} else {
|
||||
val r = page.updates.timeout(5, TimeUnit.SECONDS).take(1).toBlocking().single()
|
||||
if (r.consumed.isNotEmpty()) r.consumed.first() else r.produced.first()
|
||||
}
|
||||
assertNotNull(result)
|
||||
assertEquals(message, result.state.data.message)
|
||||
}
|
||||
}
|
||||
|
||||
private fun assertNodeRestartFailure(
|
||||
cordapps: Set<TestCorDapp>?,
|
||||
cordappsVersionAtStartup: Set<TestCorDapp>,
|
||||
cordappsVersionAtRestart: Set<TestCorDapp>,
|
||||
reuseAdditionalCordappsAtRestart: Boolean,
|
||||
assertNodeLogs: String
|
||||
) {
|
||||
@Test
|
||||
fun `restart node with incompatible version of suspended flow due to different jar name`() {
|
||||
driver(parametersForRestartingNodes()) {
|
||||
val cordapp = defaultCordapp.withName("different-jar-name-test-${UUID.randomUUID()}")
|
||||
// Create the CorDapp jar file manually first to get hold of the directory that will contain it so that we can
|
||||
// rename the filename later. The cordappDir, which acts as pointer to the jar file, does not get renamed.
|
||||
val cordappDir = TestCordappDirectories.getJarDirectory(cordapp)
|
||||
val cordappJar = cordappDir.list().single { it.toString().endsWith(".jar") }
|
||||
|
||||
return driver(DriverParameters(
|
||||
startNodesInProcess = false, // start nodes in separate processes to ensure CordappLoader is not shared between restarts
|
||||
inMemoryDB = false, // ensure database is persisted between node restarts so we can keep suspended flow in Bob's node
|
||||
cordappsForAllNodes = cordapps)
|
||||
) {
|
||||
val bobLogFolder = {
|
||||
val alice = startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, additionalCordapps = cordappsVersionAtStartup).getOrThrow()
|
||||
val bob = startNode(rpcUsers = listOf(user), providedName = BOB_NAME, additionalCordapps = cordappsVersionAtStartup).getOrThrow()
|
||||
alice.stop()
|
||||
CordaRPCClient(bob.rpcAddress).start(user.username, user.password).use {
|
||||
val flowTracker = it.proxy.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress
|
||||
// wait until Bob progresses as far as possible because Alice node is offline
|
||||
flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single()
|
||||
}
|
||||
val logFolder = bob.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME
|
||||
// SendMessageFlow suspends in Bob node
|
||||
bob.stop()
|
||||
logFolder
|
||||
}()
|
||||
createSuspendedFlowInBob(setOf(cordapp))
|
||||
|
||||
startNode(rpcUsers = listOf(user), providedName = ALICE_NAME, customOverrides = mapOf("devMode" to false),
|
||||
additionalCordapps = cordappsVersionAtRestart, regenerateCordappsOnStart = !reuseAdditionalCordappsAtRestart).getOrThrow()
|
||||
// Rename the jar file.
|
||||
cordappJar.moveTo(cordappDir / "renamed-${cordappJar.fileName}")
|
||||
|
||||
assertFailsWith(ListenProcessDeathException::class) {
|
||||
startNode(providedName = BOB_NAME, rpcUsers = listOf(user), customOverrides = mapOf("devMode" to false),
|
||||
additionalCordapps = cordappsVersionAtRestart, regenerateCordappsOnStart = !reuseAdditionalCordappsAtRestart).getOrThrow()
|
||||
}
|
||||
|
||||
val logFile = bobLogFolder.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() }
|
||||
val numberOfNodesThatLogged = logFile.readLines { it.filter { assertNodeLogs in it }.count() }
|
||||
assertEquals(1, numberOfNodesThatLogged)
|
||||
assertBobFailsToStartWithLogMessage(
|
||||
setOf(cordapp),
|
||||
CheckpointIncompatibleException.FlowNotInstalledException(SendMessageFlow::class.java).message
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restart nodes with incompatible version of suspended flow due to different jar name`() {
|
||||
fun `restart node with incompatible version of suspended flow due to different jar hash`() {
|
||||
driver(parametersForRestartingNodes()) {
|
||||
val originalCordapp = defaultCordapp.withName("different-jar-hash-test-${UUID.randomUUID()}")
|
||||
val originalCordappJar = TestCordappDirectories.getJarDirectory(originalCordapp).list().single { it.toString().endsWith(".jar") }
|
||||
|
||||
assertNodeRestartFailure(
|
||||
emptySet(),
|
||||
setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)),
|
||||
setOf(TestCorDapp.Factory.create("testJar2", "1.0", classes = classes)),
|
||||
false,
|
||||
CheckpointIncompatibleException.FlowNotInstalledException(SendMessageFlow::class.java).message)
|
||||
createSuspendedFlowInBob(setOf(originalCordapp))
|
||||
|
||||
// The vendor is part of the MANIFEST so changing it is sufficient to change the jar hash
|
||||
val modifiedCordapp = originalCordapp.withVendor("${originalCordapp.vendor}-modified")
|
||||
val modifiedCordappJar = TestCordappDirectories.getJarDirectory(modifiedCordapp).list().single { it.toString().endsWith(".jar") }
|
||||
modifiedCordappJar.moveTo(originalCordappJar, REPLACE_EXISTING)
|
||||
|
||||
assertBobFailsToStartWithLogMessage(
|
||||
setOf(originalCordapp),
|
||||
// The part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException
|
||||
"that is incompatible with the current installed version of"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restart nodes with incompatible version of suspended flow`() {
|
||||
|
||||
assertNodeRestartFailure(
|
||||
emptySet(),
|
||||
setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)),
|
||||
setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes + net.test.cordapp.v1.SendMessageFlow::class.java)),
|
||||
false,
|
||||
// the part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException
|
||||
"that is incompatible with the current installed version of")
|
||||
private fun DriverDSL.createSuspendedFlowInBob(cordapps: Set<TestCordapp>) {
|
||||
val (alice, bob) = listOf(ALICE_NAME, BOB_NAME)
|
||||
.map { startNode(providedName = it, additionalCordapps = cordapps) }
|
||||
.transpose()
|
||||
.getOrThrow()
|
||||
alice.stop()
|
||||
val flowTracker = bob.rpc.startTrackedFlow(::SendMessageFlow, message, defaultNotaryIdentity, alice.nodeInfo.singleIdentity()).progress
|
||||
// Wait until Bob progresses as far as possible because Alice node is offline
|
||||
flowTracker.takeFirst { it == SendMessageFlow.Companion.FINALISING_TRANSACTION.label }.toBlocking().single()
|
||||
bob.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restart nodes with incompatible version of suspended flow due to different timestamps only`() {
|
||||
private fun DriverDSL.assertBobFailsToStartWithLogMessage(cordapps: Collection<TestCordapp>, logMessage: String) {
|
||||
assertFailsWith(ListenProcessDeathException::class) {
|
||||
startNode(
|
||||
providedName = BOB_NAME,
|
||||
customOverrides = mapOf("devMode" to false),
|
||||
additionalCordapps = cordapps,
|
||||
regenerateCordappsOnStart = true
|
||||
).getOrThrow()
|
||||
}
|
||||
|
||||
assertNodeRestartFailure(
|
||||
emptySet(),
|
||||
setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)),
|
||||
setOf(TestCorDapp.Factory.create("testJar", "1.0", classes = classes)),
|
||||
false,
|
||||
// the part of the log message generated by CheckpointIncompatibleException.FlowVersionIncompatibleException
|
||||
"that is incompatible with the current installed version of")
|
||||
val logDir = baseDirectory(BOB_NAME) / NodeStartup.LOGS_DIRECTORY_NAME
|
||||
val logFile = logDir.list { it.filter { it.fileName.toString().endsWith(".log") }.findAny().get() }
|
||||
val matchingLineCount = logFile.readLines { it.filter { line -> logMessage in line }.count() }
|
||||
assertEquals(1, matchingLineCount)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parametersForRestartingNodes(cordappsForAllNodes: List<TestCordapp> = emptyList()): DriverParameters {
|
||||
return DriverParameters(
|
||||
startNodesInProcess = false, // Start nodes in separate processes to ensure CordappLoader is not shared between restarts
|
||||
inMemoryDB = false, // Ensure database is persisted between node restarts so we can keep suspended flows
|
||||
cordappsForAllNodes = cordappsForAllNodes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,85 @@
|
||||
package net.corda.node.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.internal.cordappForClasses
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
class FlowOverrideTests {
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class Ping(private val pongParty: Party) : FlowLogic<String>() {
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
val pongSession = initiateFlow(pongParty)
|
||||
return pongSession.sendAndReceive<String>("PING").unwrap { it }
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
open class Pong(private val pingSession: FlowSession) : FlowLogic<Unit>() {
|
||||
companion object {
|
||||
val PONG = "PONG"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send(PONG)
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
class Pong2(private val pingSession: FlowSession) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send("PONGPONG")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Ping::class)
|
||||
class Pongiest(private val pingSession: FlowSession) : Pong(pingSession) {
|
||||
|
||||
companion object {
|
||||
val GORGONZOLA = "Gorgonzola"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
pingSession.send(GORGONZOLA)
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeAClasses = setOf(Ping::class.java,
|
||||
Pong::class.java, Pongiest::class.java)
|
||||
private val nodeBClasses = setOf(Ping::class.java, Pong::class.java)
|
||||
|
||||
@Test
|
||||
fun `should use the most specific implementation of a responding flow`() {
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
|
||||
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray()))).getOrThrow()
|
||||
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
|
||||
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pongiest.GORGONZOLA))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should use the overriden implementation of a responding flow`() {
|
||||
val flowOverrides = mapOf(Ping::class.java to Pong::class.java)
|
||||
driver(DriverParameters(startNodesInProcess = true, cordappsForAllNodes = emptySet())) {
|
||||
val nodeA = startNode(additionalCordapps = setOf(cordappForClasses(*nodeAClasses.toTypedArray())), flowOverrides = flowOverrides).getOrThrow()
|
||||
val nodeB = startNode(additionalCordapps = setOf(cordappForClasses(*nodeBClasses.toTypedArray()))).getOrThrow()
|
||||
Assert.assertThat(nodeB.rpc.startFlow(::Ping, nodeA.nodeInfo.singleIdentity()).returnValue.getOrThrow(), `is`(net.corda.node.flows.FlowOverrideTests.Pong.PONG))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -2,8 +2,14 @@ package net.corda.node.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.FlowAsyncOperation
|
||||
import net.corda.core.internal.IdempotentFlow
|
||||
import net.corda.core.internal.concurrent.doneFuture
|
||||
import net.corda.core.internal.executeAsync
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
@ -16,6 +22,8 @@ import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.User
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.hibernate.exception.ConstraintViolationException
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.lang.management.ManagementFactory
|
||||
@ -51,6 +59,57 @@ class FlowRetryTest {
|
||||
assertNotNull(result)
|
||||
assertEquals("$numSessions:$numIterations", result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `async operation deduplication id is stable accross retries`() {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<AsyncRetryFlow>()))
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
notarySpecs = emptyList()
|
||||
)) {
|
||||
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::AsyncRetryFlow).returnValue.getOrThrow()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow gives up after number of exceptions, even if this is the first line of the flow`() {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<RetryFlow>()))
|
||||
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
notarySpecs = emptyList()
|
||||
)) {
|
||||
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::RetryFlow).returnValue.getOrThrow()
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow that throws in constructor throw for the RPC client that attempted to start them`() {
|
||||
val user = User("mark", "dadada", setOf(Permissions.startFlow<ThrowingFlow>()))
|
||||
assertThatExceptionOfType(CordaRuntimeException::class.java).isThrownBy {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = isQuasarAgentSpecified(),
|
||||
notarySpecs = emptyList()
|
||||
)) {
|
||||
val nodeAHandle = startNode(providedName = ALICE_NAME, rpcUsers = listOf(user)).getOrThrow()
|
||||
|
||||
val result = CordaRPCClient(nodeAHandle.rpcAddress).start(user.username, user.password).use {
|
||||
it.proxy.startFlow(::ThrowingFlow).returnValue.getOrThrow()
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isQuasarAgentSpecified(): Boolean {
|
||||
@ -60,6 +119,8 @@ fun isQuasarAgentSpecified(): Boolean {
|
||||
|
||||
class ExceptionToCauseRetry : SQLException("deadlock")
|
||||
|
||||
class ExceptionToCauseFiniteRetry : ConstraintViolationException("Faked violation", SQLException("Fake"), "Fake name")
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class InitiatorFlow(private val sessionsCount: Int, private val iterationsCount: Int, private val other: Party) : FlowLogic<Any>() {
|
||||
@ -157,3 +218,72 @@ data class SessionInfo(val sessionNum: Int, val iterationsCount: Int)
|
||||
enum class Step { First, BeforeInitiate, AfterInitiate, AfterInitiateSendReceive, BeforeSend, AfterSend, BeforeReceive, AfterReceive }
|
||||
|
||||
data class Visited(val sessionNum: Int, val iterationNum: Int, val step: Step)
|
||||
|
||||
@StartableByRPC
|
||||
class RetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
companion object {
|
||||
object FIRST_STEP : ProgressTracker.Step("Step one")
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
progressTracker.currentStep = FIRST_STEP
|
||||
throw ExceptionToCauseFiniteRetry()
|
||||
return "Result"
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class AsyncRetryFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
companion object {
|
||||
object FIRST_STEP : ProgressTracker.Step("Step one")
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
|
||||
val deduplicationIds = mutableSetOf<String>()
|
||||
}
|
||||
|
||||
class RecordDeduplicationId: FlowAsyncOperation<String> {
|
||||
override fun execute(deduplicationId: String): CordaFuture<String> {
|
||||
val dedupeIdIsNew = deduplicationIds.add(deduplicationId)
|
||||
if (dedupeIdIsNew) {
|
||||
throw ExceptionToCauseFiniteRetry()
|
||||
}
|
||||
return doneFuture(deduplicationId)
|
||||
}
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
progressTracker.currentStep = FIRST_STEP
|
||||
executeAsync(RecordDeduplicationId())
|
||||
return "Result"
|
||||
}
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class ThrowingFlow() : FlowLogic<String>(), IdempotentFlow {
|
||||
companion object {
|
||||
object FIRST_STEP : ProgressTracker.Step("Step one")
|
||||
|
||||
fun tracker() = ProgressTracker(FIRST_STEP)
|
||||
}
|
||||
|
||||
override val progressTracker = tracker()
|
||||
|
||||
init {
|
||||
throw IllegalStateException("This flow can never be ")
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): String {
|
||||
progressTracker.currentStep = FIRST_STEP
|
||||
return "Result"
|
||||
}
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
package net.corda.node.modes.draining
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID
|
||||
import net.corda.testMessage.Message
|
||||
import net.corda.testMessage.MessageContract
|
||||
import net.corda.testMessage.MessageState
|
||||
import net.corda.RpcInfo
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.core.contracts.Command
|
||||
import net.corda.core.contracts.StateAndContract
|
||||
import net.corda.core.flows.*
|
||||
@ -15,9 +13,11 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.RpcInfo
|
||||
import net.corda.client.rpc.CordaRPCClient
|
||||
import net.corda.node.services.Permissions.Companion.all
|
||||
import net.corda.testMessage.MESSAGE_CONTRACT_PROGRAM_ID
|
||||
import net.corda.testMessage.Message
|
||||
import net.corda.testMessage.MessageContract
|
||||
import net.corda.testMessage.MessageState
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
@ -53,7 +53,11 @@ class FlowsDrainingModeContentionTest {
|
||||
@Test
|
||||
fun `draining mode does not deadlock with acks between 2 nodes`() {
|
||||
val message = "Ground control to Major Tom"
|
||||
driver(DriverParameters(startNodesInProcess = true, portAllocation = portAllocation, extraCordappPackagesToScan = listOf(MessageState::class.packageName))) {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
portAllocation = portAllocation,
|
||||
extraCordappPackagesToScan = listOf(MessageState::class.packageName)
|
||||
)) {
|
||||
val nodeA = startNode(providedName = ALICE_NAME, rpcUsers = users).getOrThrow()
|
||||
val nodeB = startNode(providedName = BOB_NAME, rpcUsers = users).getOrThrow()
|
||||
|
||||
@ -70,11 +74,12 @@ class FlowsDrainingModeContentionTest {
|
||||
|
||||
@StartableByRPC
|
||||
@InitiatingFlow
|
||||
class ProposeTransactionAndWaitForCommit(private val data: String, private val myRpcInfo: RpcInfo, private val counterParty: Party, private val notary: Party) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
class ProposeTransactionAndWaitForCommit(private val data: String,
|
||||
private val myRpcInfo: RpcInfo,
|
||||
private val counterParty: Party,
|
||||
private val notary: Party) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
|
||||
val session = initiateFlow(counterParty)
|
||||
val messageState = MessageState(message = Message(data), by = ourIdentity)
|
||||
val command = Command(MessageContract.Commands.Send(), messageState.participants.map { it.owningKey })
|
||||
@ -91,10 +96,8 @@ class ProposeTransactionAndWaitForCommit(private val data: String, private val m
|
||||
|
||||
@InitiatedBy(ProposeTransactionAndWaitForCommit::class)
|
||||
class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
|
||||
val tx = subFlow(ReceiveTransactionFlow(session))
|
||||
val signedTx = serviceHub.addSignature(tx)
|
||||
val initiatingRpcInfo = session.receive<RpcInfo>().unwrap { it }
|
||||
@ -105,9 +108,8 @@ class SignTransactionTriggerDrainingModeAndFinality(private val session: FlowSes
|
||||
}
|
||||
|
||||
private fun triggerDrainingModeForInitiatingNode(initiatingRpcInfo: RpcInfo) {
|
||||
|
||||
CordaRPCClient(initiatingRpcInfo.address).start(initiatingRpcInfo.username, initiatingRpcInfo.password).use {
|
||||
it.proxy.setFlowsDrainingModeEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,17 +3,11 @@ package net.corda.node.services
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.CordaRuntimeException
|
||||
import net.corda.core.contracts.Contract
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionState
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.cordapp.CordappProvider
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.internal.toLedgerTransaction
|
||||
import net.corda.core.node.NetworkParameters
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
@ -21,20 +15,16 @@ import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.SerializationFactory
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.DUMMY_NOTARY_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.core.TestIdentity
|
||||
import net.corda.testing.driver.DriverDSL
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.NodeHandle
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.internal.MockCordappConfigProvider
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
@ -60,7 +50,6 @@ class AttachmentLoadingTests {
|
||||
private val appContext get() = provider.getAppContext(cordapp)
|
||||
|
||||
private companion object {
|
||||
private val logger = contextLogger()
|
||||
val isolatedJAR = AttachmentLoadingTests::class.java.getResource("isolated.jar")!!
|
||||
const val ISOLATED_CONTRACT_ID = "net.corda.finance.contracts.isolated.AnotherDummyContract"
|
||||
|
||||
@ -71,12 +60,6 @@ class AttachmentLoadingTests {
|
||||
.asSubclass(FlowLogic::class.java)
|
||||
val DUMMY_BANK_A = TestIdentity(DUMMY_BANK_A_NAME, 40).party
|
||||
val DUMMY_NOTARY = TestIdentity(DUMMY_NOTARY_NAME, 20).party
|
||||
private fun DriverDSL.createTwoNodes(): List<NodeHandle> {
|
||||
return listOf(
|
||||
startNode(providedName = bankAName),
|
||||
startNode(providedName = bankBName)
|
||||
).transpose().getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private val services = object : ServicesForResolution {
|
||||
|
@ -1,221 +0,0 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import com.nhaarman.mockito_kotlin.doReturn
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.AlwaysAcceptAttachmentConstraint
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.deleteIfExists
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.node.NotaryInfo
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.node.services.transactions.minClusterSize
|
||||
import net.corda.node.services.transactions.minCorrectReplicas
|
||||
import net.corda.nodeapi.internal.DevIdentityGenerator
|
||||
import net.corda.nodeapi.internal.network.NetworkParametersCopier
|
||||
import net.corda.testing.common.internal.testNetworkParameters
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.TestClock
|
||||
import net.corda.testing.node.internal.*
|
||||
import org.hamcrest.Matchers.instanceOf
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertThat
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class BFTNotaryServiceTests {
|
||||
companion object {
|
||||
private lateinit var mockNet: InternalMockNetwork
|
||||
private lateinit var notary: Party
|
||||
private lateinit var node: TestStartedNode
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun before() {
|
||||
mockNet = InternalMockNetwork(cordappsForAllNodes = cordappsForPackages("net.corda.testing.contracts"))
|
||||
val clusterSize = minClusterSize(1)
|
||||
val started = startBftClusterAndNode(clusterSize, mockNet)
|
||||
notary = started.first
|
||||
node = started.second
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun stopNodes() {
|
||||
mockNet.stopNodes()
|
||||
}
|
||||
|
||||
fun startBftClusterAndNode(clusterSize: Int, mockNet: InternalMockNetwork, exposeRaces: Boolean = false): Pair<Party, TestStartedNode> {
|
||||
(Paths.get("config") / "currentView").deleteIfExists() // XXX: Make config object warn if this exists?
|
||||
val replicaIds = (0 until clusterSize)
|
||||
|
||||
val notaryIdentity = DevIdentityGenerator.generateDistributedNotaryCompositeIdentity(
|
||||
replicaIds.map { mockNet.baseDirectory(mockNet.nextNodeId + it) },
|
||||
CordaX500Name("BFT", "Zurich", "CH"))
|
||||
|
||||
val networkParameters = NetworkParametersCopier(testNetworkParameters(listOf(NotaryInfo(notaryIdentity, false))))
|
||||
|
||||
val clusterAddresses = replicaIds.map { NetworkHostAndPort("localhost", 11000 + it * 10) }
|
||||
|
||||
val nodes = replicaIds.map { replicaId ->
|
||||
mockNet.createUnstartedNode(InternalMockNodeParameters(configOverrides = {
|
||||
val notary = NotaryConfig(validating = false, bftSMaRt = BFTSMaRtConfiguration(replicaId, clusterAddresses, exposeRaces = exposeRaces))
|
||||
doReturn(notary).whenever(it).notary
|
||||
}))
|
||||
} + mockNet.createUnstartedNode()
|
||||
|
||||
// MockNetwork doesn't support BFT clusters, so we create all the nodes we need unstarted, and then install the
|
||||
// network-parameters in their directories before they're started.
|
||||
val node = nodes.map { node ->
|
||||
networkParameters.install(mockNet.baseDirectory(node.id))
|
||||
node.start()
|
||||
}.last()
|
||||
|
||||
return Pair(notaryIdentity, node)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `detect double spend`() {
|
||||
node.run {
|
||||
val issueTx = signInitialTransaction(notary) {
|
||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||
}
|
||||
services.recordTransactions(issueTx)
|
||||
val spendTxs = (1..10).map {
|
||||
signInitialTransaction(notary) {
|
||||
addInputState(issueTx.tx.outRef<ContractState>(0))
|
||||
}
|
||||
}
|
||||
assertEquals(spendTxs.size, spendTxs.map { it.id }.distinct().size)
|
||||
val flows = spendTxs.map { NotaryFlow.Client(it) }
|
||||
val stateMachines = flows.map { services.startFlow(it) }
|
||||
mockNet.runNetwork()
|
||||
val results = stateMachines.map { Try.on { it.resultFuture.getOrThrow() } }
|
||||
val successfulIndex = results.mapIndexedNotNull { index, result ->
|
||||
if (result is Try.Success) {
|
||||
val signers = result.value.map { it.by }
|
||||
assertEquals(minCorrectReplicas(3), signers.size)
|
||||
signers.forEach {
|
||||
assertTrue(it in (notary.owningKey as CompositeKey).leafKeys)
|
||||
}
|
||||
index
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.single()
|
||||
spendTxs.zip(results).forEach { (tx, result) ->
|
||||
if (result is Try.Failure) {
|
||||
val exception = result.exception as NotaryException
|
||||
val error = exception.error as NotaryError.Conflict
|
||||
assertEquals(tx.id, error.txId)
|
||||
val (stateRef, cause) = error.consumedStates.entries.single()
|
||||
assertEquals(StateRef(issueTx.id, 0), stateRef)
|
||||
assertEquals(spendTxs[successfulIndex].id.sha256(), cause.hashOfTransactionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transactions outside their time window are rejected`() {
|
||||
node.run {
|
||||
val issueTx = signInitialTransaction(notary) {
|
||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||
}
|
||||
services.recordTransactions(issueTx)
|
||||
val spendTx = signInitialTransaction(notary) {
|
||||
addInputState(issueTx.tx.outRef<ContractState>(0))
|
||||
setTimeWindow(TimeWindow.fromOnly(Instant.MAX))
|
||||
}
|
||||
val flow = NotaryFlow.Client(spendTx)
|
||||
val resultFuture = services.startFlow(flow).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val exception = assertFailsWith<ExecutionException> { resultFuture.get() }
|
||||
assertThat(exception.cause, instanceOf(NotaryException::class.java))
|
||||
val error = (exception.cause as NotaryException).error
|
||||
assertThat(error, instanceOf(NotaryError.TimeWindowInvalid::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notarise issue tx with time-window`() {
|
||||
node.run {
|
||||
val issueTx = signInitialTransaction(notary) {
|
||||
setTimeWindow(services.clock.instant(), 30.seconds)
|
||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||
}
|
||||
val resultFuture = services.startFlow(NotaryFlow.Client(issueTx)).resultFuture
|
||||
|
||||
mockNet.runNetwork()
|
||||
val signatures = resultFuture.get()
|
||||
verifySignatures(signatures, issueTx.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transactions can be re-notarised outside their time window`() {
|
||||
node.run {
|
||||
val issueTx = signInitialTransaction(notary) {
|
||||
addOutputState(DummyContract.SingleOwnerState(owner = info.singleIdentity()), DummyContract.PROGRAM_ID, AlwaysAcceptAttachmentConstraint)
|
||||
}
|
||||
services.recordTransactions(issueTx)
|
||||
val spendTx = signInitialTransaction(notary) {
|
||||
addInputState(issueTx.tx.outRef<ContractState>(0))
|
||||
setTimeWindow(TimeWindow.untilOnly(Instant.now() + Duration.ofHours(1)))
|
||||
}
|
||||
val resultFuture = services.startFlow(NotaryFlow.Client(spendTx)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val signatures = resultFuture.get()
|
||||
verifySignatures(signatures, spendTx.id)
|
||||
|
||||
for (node in mockNet.nodes) {
|
||||
(node.started!!.services.clock as TestClock).advanceBy(Duration.ofDays(1))
|
||||
}
|
||||
|
||||
val resultFuture2 = services.startFlow(NotaryFlow.Client(spendTx)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
val signatures2 = resultFuture2.get()
|
||||
verifySignatures(signatures2, spendTx.id)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifySignatures(signatures: List<TransactionSignature>, txId: SecureHash) {
|
||||
notary.owningKey.isFulfilledBy(signatures.map { it.by })
|
||||
signatures.forEach { it.verify(txId) }
|
||||
}
|
||||
|
||||
private fun TestStartedNode.signInitialTransaction(notary: Party, block: TransactionBuilder.() -> Any?): SignedTransaction {
|
||||
return services.signInitialTransaction(
|
||||
TransactionBuilder(notary).apply {
|
||||
addCommand(dummyCommand(services.myInfo.singleIdentity().owningKey))
|
||||
block()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.NotaryException
|
||||
import net.corda.core.flows.NotaryFlow
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.testing.contracts.DummyContract
|
||||
import net.corda.testing.core.DUMMY_BANK_A_NAME
|
||||
import net.corda.testing.core.dummyCommand
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.driver.DriverParameters
|
||||
import net.corda.testing.driver.InProcess
|
||||
import net.corda.testing.driver.driver
|
||||
import net.corda.testing.node.ClusterSpec
|
||||
import net.corda.testing.node.NotarySpec
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class RaftNotaryServiceTests {
|
||||
private val notaryName = CordaX500Name("RAFT Notary Service", "London", "GB")
|
||||
|
||||
@Test
|
||||
fun `detect double spend`() {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
|
||||
notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))
|
||||
)) {
|
||||
val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow()
|
||||
val inputState = issueState(bankA, defaultNotaryIdentity)
|
||||
|
||||
val firstTxBuilder = TransactionBuilder(defaultNotaryIdentity)
|
||||
.addInputState(inputState)
|
||||
.addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey))
|
||||
val firstSpendTx = bankA.services.signInitialTransaction(firstTxBuilder)
|
||||
|
||||
val firstSpend = bankA.startFlow(NotaryFlow.Client(firstSpendTx))
|
||||
firstSpend.getOrThrow()
|
||||
|
||||
val secondSpendBuilder = TransactionBuilder(defaultNotaryIdentity).withItems(inputState).run {
|
||||
val dummyState = DummyContract.SingleOwnerState(0, bankA.services.myInfo.singleIdentity())
|
||||
addOutputState(dummyState, DummyContract.PROGRAM_ID)
|
||||
addCommand(dummyCommand(bankA.services.myInfo.singleIdentity().owningKey))
|
||||
this
|
||||
}
|
||||
val secondSpendTx = bankA.services.signInitialTransaction(secondSpendBuilder)
|
||||
val secondSpend = bankA.startFlow(NotaryFlow.Client(secondSpendTx))
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { secondSpend.getOrThrow() }
|
||||
val error = ex.error as NotaryError.Conflict
|
||||
assertEquals(error.txId, secondSpendTx.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `notarise issue tx with time-window`() {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.testing.contracts"),
|
||||
notarySpecs = listOf(NotarySpec(notaryName, cluster = ClusterSpec.Raft(clusterSize = 3)))
|
||||
)) {
|
||||
val bankA = startNode(providedName = DUMMY_BANK_A_NAME).map { (it as InProcess) }.getOrThrow()
|
||||
val builder = DummyContract.generateInitial(Random().nextInt(), defaultNotaryIdentity, bankA.services.myInfo.singleIdentity().ref(0))
|
||||
.setTimeWindow(bankA.services.clock.instant(), 30.seconds)
|
||||
val issueTx = bankA.services.signInitialTransaction(builder)
|
||||
|
||||
bankA.startFlow(NotaryFlow.Client(issueTx)).getOrThrow()
|
||||
}
|
||||
}
|
||||
|
||||
private fun issueState(nodeHandle: InProcess, notary: Party): StateAndRef<*> {
|
||||
val builder = DummyContract.generateInitial(Random().nextInt(), notary, nodeHandle.services.myInfo.singleIdentity().ref(0))
|
||||
val stx = nodeHandle.services.signInitialTransaction(builder)
|
||||
nodeHandle.services.recordTransactions(stx)
|
||||
return StateAndRef(stx.coreTransaction.outputs.first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
@ -42,7 +42,7 @@ class DistributedServiceTests {
|
||||
invokeRpc(CordaRPCOps::stateMachinesFeed))
|
||||
)
|
||||
driver(DriverParameters(
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas"),
|
||||
extraCordappPackagesToScan = listOf("net.corda.finance.contracts", "net.corda.finance.schemas", "net.corda.notary.raft"),
|
||||
notarySpecs = listOf(
|
||||
NotarySpec(
|
||||
DUMMY_NOTARY_NAME,
|
||||
|
@ -32,11 +32,14 @@ import kotlin.test.assertEquals
|
||||
|
||||
class ScheduledFlowIntegrationTests {
|
||||
@StartableByRPC
|
||||
class InsertInitialStateFlow(private val destination: Party, private val notary: Party, private val identity: Int = 1, private val scheduledFor: Instant? = null) : FlowLogic<Unit>() {
|
||||
class InsertInitialStateFlow(private val destination: Party,
|
||||
private val notary: Party,
|
||||
private val identity: Int = 1,
|
||||
private val scheduledFor: Instant? = null) : FlowLogic<Unit>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val scheduledState = ScheduledState(scheduledFor
|
||||
?: serviceHub.clock.instant(), ourIdentity, destination, identity.toString())
|
||||
val creationTime = scheduledFor ?: serviceHub.clock.instant()
|
||||
val scheduledState = ScheduledState(creationTime, ourIdentity, destination, identity.toString())
|
||||
val builder = TransactionBuilder(notary)
|
||||
.addOutputState(scheduledState, DummyContract.PROGRAM_ID)
|
||||
.addCommand(dummyCommand(ourIdentity.owningKey))
|
||||
@ -90,8 +93,20 @@ class ScheduledFlowIntegrationTests {
|
||||
val scheduledFor = Instant.now().plusSeconds(10)
|
||||
val initialiseFutures = mutableListOf<CordaFuture<*>>()
|
||||
for (i in 0 until N) {
|
||||
initialiseFutures.add(aliceClient.proxy.startFlow(::InsertInitialStateFlow, bob.nodeInfo.legalIdentities.first(), defaultNotaryIdentity, i, scheduledFor).returnValue)
|
||||
initialiseFutures.add(bobClient.proxy.startFlow(::InsertInitialStateFlow, alice.nodeInfo.legalIdentities.first(), defaultNotaryIdentity, i + 100, scheduledFor).returnValue)
|
||||
initialiseFutures.add(aliceClient.proxy.startFlow(
|
||||
::InsertInitialStateFlow,
|
||||
bob.nodeInfo.legalIdentities.first(),
|
||||
defaultNotaryIdentity,
|
||||
i,
|
||||
scheduledFor
|
||||
).returnValue)
|
||||
initialiseFutures.add(bobClient.proxy.startFlow(
|
||||
::InsertInitialStateFlow,
|
||||
alice.nodeInfo.legalIdentities.first(),
|
||||
defaultNotaryIdentity,
|
||||
i + 100,
|
||||
scheduledFor
|
||||
).returnValue)
|
||||
}
|
||||
initialiseFutures.getOrThrowAll()
|
||||
|
||||
|
@ -7,14 +7,12 @@ import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.config.FlowTimeoutConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.network.PersistentNetworkMapCache
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
@ -22,6 +20,8 @@ import net.corda.testing.core.MAX_MESSAGE_SIZE
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.stubs.CertificateStoreStubs
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
|
@ -47,18 +47,22 @@ class NetworkMapTest(var initFunc: (URL, NetworkMapServer) -> CompatibilityZoneP
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun runParams() = listOf(
|
||||
{ addr: URL, nms: NetworkMapServer ->
|
||||
SharedCompatibilityZoneParams(
|
||||
{
|
||||
addr: URL,
|
||||
nms: NetworkMapServer -> SharedCompatibilityZoneParams(
|
||||
addr,
|
||||
pnm = null,
|
||||
publishNotaries = {
|
||||
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
||||
}
|
||||
)
|
||||
},
|
||||
{ addr: URL, nms: NetworkMapServer ->
|
||||
SplitCompatibilityZoneParams(
|
||||
{
|
||||
addr: URL,
|
||||
nms: NetworkMapServer -> SplitCompatibilityZoneParams (
|
||||
doormanURL = URL("http://I/Don't/Exist"),
|
||||
networkMapURL = addr,
|
||||
pnm = null,
|
||||
publishNotaries = {
|
||||
nms.networkParameters = testNetworkParameters(it, modifiedTime = Instant.ofEpochMilli(random63BitValue()), epoch = 2)
|
||||
}
|
||||
|
@ -2,13 +2,13 @@ package net.corda.node.services.network
|
||||
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
||||
import net.corda.node.services.identity.InMemoryIdentityService
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.DEV_ROOT_CA
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.*
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
|
@ -23,6 +23,8 @@ import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.TestingNamedCacheFactory
|
||||
import net.corda.testing.internal.fromUserList
|
||||
import net.corda.testing.internal.p2pSslOptions
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQConnectionTimedOutException
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
@ -128,7 +130,7 @@ class ArtemisRpcTests {
|
||||
|
||||
private fun <OPS : RPCOps> InternalRPCMessagingClient.start(ops: OPS, securityManager: RPCSecurityManager, brokerControl: ActiveMQServerControl) {
|
||||
apply {
|
||||
init(ops, securityManager)
|
||||
init(ops, securityManager, TestingNamedCacheFactory())
|
||||
start(brokerControl)
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,11 @@ import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.node.internal.NodeFlowManager
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.BOB_NAME
|
||||
import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.internal.NodeBasedTest
|
||||
import net.corda.testing.node.internal.startFlow
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
@ -18,9 +19,10 @@ import org.junit.Test
|
||||
class FlowVersioningTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `getFlowContext returns the platform version for core flows`() {
|
||||
val bobFlowManager = NodeFlowManager()
|
||||
val alice = startNode(ALICE_NAME, platformVersion = 2)
|
||||
val bob = startNode(BOB_NAME, platformVersion = 3)
|
||||
bob.node.installCoreFlow(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
|
||||
val bob = startNode(BOB_NAME, platformVersion = 3, flowManager = bobFlowManager)
|
||||
bobFlowManager.registerInitiatedCoreFlowFactory(PretendInitiatingCoreFlow::class, ::PretendInitiatedCoreFlow)
|
||||
val (alicePlatformVersionAccordingToBob, bobPlatformVersionAccordingToAlice) = alice.services.startFlow(
|
||||
PretendInitiatingCoreFlow(bob.info.singleIdentity())).resultFuture.getOrThrow()
|
||||
assertThat(alicePlatformVersionAccordingToBob).isEqualTo(2)
|
||||
@ -45,4 +47,5 @@ class FlowVersioningTest : NodeBasedTest() {
|
||||
@Suspendable
|
||||
override fun call() = otherSideSession.send(otherSideSession.getCounterpartyFlowInfo().flowVersion)
|
||||
}
|
||||
|
||||
}
|
@ -1,180 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import io.atomix.catalyst.transport.Address
|
||||
import io.atomix.copycat.client.ConnectionStrategies
|
||||
import io.atomix.copycat.client.CopycatClient
|
||||
import io.atomix.copycat.server.CopycatServer
|
||||
import io.atomix.copycat.server.storage.Storage
|
||||
import io.atomix.copycat.server.storage.StorageLevel
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.internal.concurrent.asCordaFuture
|
||||
import net.corda.core.internal.concurrent.transpose
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.node.internal.configureDatabase
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.utilities.TestingNamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.driver.PortAllocation
|
||||
import net.corda.testing.internal.LogHelper
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
import org.hamcrest.Matchers.instanceOf
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertThat
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
class RaftTransactionCommitLogTests {
|
||||
data class Member(val client: CopycatClient, val server: CopycatServer)
|
||||
|
||||
@Rule
|
||||
@JvmField
|
||||
val testSerialization = SerializationEnvironmentRule(true)
|
||||
|
||||
private val databases: MutableList<CordaPersistence> = mutableListOf()
|
||||
private val portAllocation = PortAllocation.Incremental(10000)
|
||||
|
||||
private lateinit var cluster: List<Member>
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
LogHelper.setLevel("-org.apache.activemq")
|
||||
LogHelper.setLevel("+io.atomix")
|
||||
cluster = setUpCluster()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
LogHelper.reset("org.apache.activemq", "io.atomix")
|
||||
cluster.map { it.client.close().asCordaFuture() }.transpose().getOrThrow()
|
||||
cluster.map { it.server.shutdown().asCordaFuture() }.transpose().getOrThrow()
|
||||
databases.forEach { it.close() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `stores entries correctly`() {
|
||||
val client = cluster.last().client
|
||||
|
||||
val states = listOf(StateRef(SecureHash.randomSHA256(), 0), StateRef(SecureHash.randomSHA256(), 0))
|
||||
val txId: SecureHash = SecureHash.randomSHA256()
|
||||
val requestingPartyName = ALICE_NAME
|
||||
val requestSignature = ByteArray(1024)
|
||||
|
||||
val commitCommand = RaftTransactionCommitLog.Commands.CommitTransaction(states, txId, requestingPartyName.toString(), requestSignature)
|
||||
val commitError = client.submit(commitCommand).getOrThrow()
|
||||
assertNull(commitError)
|
||||
|
||||
val value1 = client.submit(RaftTransactionCommitLog.Commands.Get(states[0]))
|
||||
val value2 = client.submit(RaftTransactionCommitLog.Commands.Get(states[1]))
|
||||
|
||||
assertEquals(value1.getOrThrow(), txId)
|
||||
assertEquals(value2.getOrThrow(), txId)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns conflict for duplicate entries`() {
|
||||
val client = cluster.last().client
|
||||
|
||||
val states = listOf(StateRef(SecureHash.randomSHA256(), 0), StateRef(SecureHash.randomSHA256(), 0))
|
||||
val txIdFirst = SecureHash.randomSHA256()
|
||||
val txIdSecond = SecureHash.randomSHA256()
|
||||
val requestingPartyName = ALICE_NAME
|
||||
val requestSignature = ByteArray(1024)
|
||||
|
||||
val commitCommandFirst = RaftTransactionCommitLog.Commands.CommitTransaction(states, txIdFirst, requestingPartyName.toString(), requestSignature)
|
||||
var commitError = client.submit(commitCommandFirst).getOrThrow()
|
||||
assertNull(commitError)
|
||||
|
||||
val commitCommandSecond = RaftTransactionCommitLog.Commands.CommitTransaction(states, txIdSecond, requestingPartyName.toString(), requestSignature)
|
||||
commitError = client.submit(commitCommandSecond).getOrThrow()
|
||||
val conflict = commitError as NotaryError.Conflict
|
||||
assertEquals(states.toSet(), conflict.consumedStates.keys)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transactions outside their time window are rejected`() {
|
||||
val client = cluster.last().client
|
||||
|
||||
val states = listOf(StateRef(SecureHash.randomSHA256(), 0), StateRef(SecureHash.randomSHA256(), 0))
|
||||
val txId: SecureHash = SecureHash.randomSHA256()
|
||||
val requestingPartyName = ALICE_NAME
|
||||
val requestSignature = ByteArray(1024)
|
||||
val timeWindow = TimeWindow.fromOnly(Instant.MAX)
|
||||
|
||||
val commitCommand = RaftTransactionCommitLog.Commands.CommitTransaction(
|
||||
states, txId, requestingPartyName.toString(), requestSignature, timeWindow
|
||||
)
|
||||
val commitError = client.submit(commitCommand).getOrThrow()
|
||||
assertThat(commitError, instanceOf(NotaryError.TimeWindowInvalid::class.java))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `transactions can be re-notarised outside their time window`() {
|
||||
val client = cluster.last().client
|
||||
|
||||
val states = listOf(StateRef(SecureHash.randomSHA256(), 0), StateRef(SecureHash.randomSHA256(), 0))
|
||||
val txId: SecureHash = SecureHash.randomSHA256()
|
||||
val requestingPartyName = ALICE_NAME
|
||||
val requestSignature = ByteArray(1024)
|
||||
val timeWindow = TimeWindow.fromOnly(Instant.MIN)
|
||||
|
||||
val commitCommand = RaftTransactionCommitLog.Commands.CommitTransaction(
|
||||
states, txId, requestingPartyName.toString(), requestSignature, timeWindow
|
||||
)
|
||||
val commitError = client.submit(commitCommand).getOrThrow()
|
||||
assertNull(commitError)
|
||||
|
||||
val expiredTimeWindow = TimeWindow.untilOnly(Instant.MIN)
|
||||
val commitCommand2 = RaftTransactionCommitLog.Commands.CommitTransaction(
|
||||
states, txId, requestingPartyName.toString(), requestSignature, expiredTimeWindow
|
||||
)
|
||||
val commitError2 = client.submit(commitCommand2).getOrThrow()
|
||||
assertNull(commitError2)
|
||||
}
|
||||
|
||||
private fun setUpCluster(nodeCount: Int = 3): List<Member> {
|
||||
val clusterAddress = portAllocation.nextHostAndPort()
|
||||
val cluster = mutableListOf(createReplica(clusterAddress))
|
||||
for (i in 1..nodeCount) cluster.add(createReplica(portAllocation.nextHostAndPort(), clusterAddress))
|
||||
return cluster.map { it.getOrThrow() }
|
||||
}
|
||||
|
||||
private fun createReplica(myAddress: NetworkHostAndPort, clusterAddress: NetworkHostAndPort? = null): CompletableFuture<Member> {
|
||||
val storage = Storage.builder().withStorageLevel(StorageLevel.MEMORY).build()
|
||||
val address = Address(myAddress.host, myAddress.port)
|
||||
val database = configureDatabase(makeTestDataSourceProperties(), DatabaseConfig(), { null }, { null }, NodeSchemaService(includeNotarySchemas = true))
|
||||
databases.add(database)
|
||||
val stateMachineFactory = { RaftTransactionCommitLog(database, Clock.systemUTC(), { RaftUniquenessProvider.createMap(TestingNamedCacheFactory()) }) }
|
||||
|
||||
val server = CopycatServer.builder(address)
|
||||
.withStateMachine(stateMachineFactory)
|
||||
.withStorage(storage)
|
||||
.withSerializer(RaftTransactionCommitLog.serializer)
|
||||
.build()
|
||||
|
||||
val serverInitFuture = if (clusterAddress != null) {
|
||||
val cluster = Address(clusterAddress.host, clusterAddress.port)
|
||||
server.join(cluster)
|
||||
} else {
|
||||
server.bootstrap()
|
||||
}
|
||||
|
||||
val client = CopycatClient.builder(address)
|
||||
.withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF)
|
||||
.withSerializer(RaftTransactionCommitLog.serializer)
|
||||
.build()
|
||||
return serverInitFuture.thenCompose { client.connect(address) }.thenApply { Member(it, server) }
|
||||
}
|
||||
}
|
@ -81,6 +81,7 @@ class NodeRegistrationTest {
|
||||
fun `node registration correct root cert`() {
|
||||
val compatibilityZone = SharedCompatibilityZoneParams(
|
||||
URL("http://$serverHostAndPort"),
|
||||
null,
|
||||
publishNotaries = { server.networkParameters = testNetworkParameters(it) },
|
||||
rootCert = DEV_ROOT_CA.certificate)
|
||||
internalDriver(
|
||||
|
@ -105,11 +105,11 @@ class MQSecurityAsNodeTest : P2PMQSecurityTest() {
|
||||
val clientTLSCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientKeyPair, CordaX500Name("MiniCorp", "London", "GB").x500Principal, tlsKeyPair.public)
|
||||
|
||||
signingCertStore.get(createNew = true).update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_CA, clientKeyPair.private, listOf(clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate), signingCertStore.entryPassword)
|
||||
}
|
||||
|
||||
p2pSslConfig.keyStore.get(createNew = true).update {
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate))
|
||||
setPrivateKey(X509Utilities.CORDA_CLIENT_TLS, tlsKeyPair.private, listOf(clientTLSCert, clientCACert, DEV_INTERMEDIATE_CA.certificate, DEV_ROOT_CA.certificate), p2pSslConfig.keyStore.entryPassword)
|
||||
}
|
||||
|
||||
val attacker = clientTo(alice.node.configuration.p2pAddress, p2pSslConfig)
|
||||
|
@ -40,6 +40,7 @@ class P2PMessagingTest {
|
||||
private fun startDriverWithDistributedService(dsl: DriverDSL.(List<InProcess>) -> Unit) {
|
||||
driver(DriverParameters(
|
||||
startNodesInProcess = true,
|
||||
extraCordappPackagesToScan = listOf("net.corda.notary.raft"),
|
||||
notarySpecs = listOf(NotarySpec(DISTRIBUTED_SERVICE_NAME, cluster = ClusterSpec.Raft(clusterSize = 2)))
|
||||
)) {
|
||||
dsl(defaultNotaryHandle.nodeHandles.getOrThrow().map { (it as InProcess) })
|
||||
|
@ -46,12 +46,12 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p
|
||||
|
||||
progressTracker.currentStep = FINALISING_TRANSACTION
|
||||
|
||||
if (reciepent != null) {
|
||||
return if (reciepent != null) {
|
||||
val session = initiateFlow(reciepent)
|
||||
subFlow(SendTransactionFlow(session, signedTx))
|
||||
return subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
subFlow(FinalityFlow(signedTx, setOf(reciepent), FINALISING_TRANSACTION.childProgressTracker()))
|
||||
} else {
|
||||
return subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
||||
subFlow(FinalityFlow(signedTx, FINALISING_TRANSACTION.childProgressTracker()))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,10 +59,9 @@ class SendMessageFlow(private val message: Message, private val notary: Party, p
|
||||
|
||||
@InitiatedBy(SendMessageFlow::class)
|
||||
class Record(private val session: FlowSession) : FlowLogic<Unit>() {
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val tx = subFlow(ReceiveTransactionFlow(session, statesToRecord = StatesToRecord.ALL_VISIBLE))
|
||||
serviceHub.addSignature(tx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,11 +4,11 @@
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.cliutils.start
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.NodeStartupCli
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
// Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass.
|
||||
// It will exit the process in case of startup failure and is not intended to be used by embedders. If you want
|
||||
// to embed Node in your own container, instantiate it directly and set up the configuration objects yourself.
|
||||
NodeStartup().start(args)
|
||||
NodeStartupCli().start(args)
|
||||
}
|
||||
|
@ -2,18 +2,18 @@ package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfigurationImpl
|
||||
import net.corda.node.services.config.parseAsNodeConfiguration
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import picocli.CommandLine.Option
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeCmdLineOptions {
|
||||
open class SharedNodeCmdLineOptions {
|
||||
@Option(
|
||||
names = ["-b", "--base-directory"],
|
||||
description = ["The node working directory where all the files are kept."]
|
||||
@ -27,6 +27,53 @@ class NodeCmdLineOptions {
|
||||
private var _configFile: Path? = null
|
||||
val configFile: Path get() = _configFile ?: (baseDirectory / "node.conf")
|
||||
|
||||
@Option(
|
||||
names = ["--on-unknown-config-keys"],
|
||||
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
|
||||
)
|
||||
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--dev-mode"],
|
||||
description = ["Runs the node in development mode. Unsafe for production."]
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
open fun loadConfig(): NodeConfiguration {
|
||||
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle)
|
||||
}
|
||||
|
||||
protected fun getRawConfig(): Config {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile
|
||||
)
|
||||
if (devMode == true) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
return rawConfig
|
||||
}
|
||||
|
||||
fun copyFrom(other: SharedNodeCmdLineOptions) {
|
||||
baseDirectory = other.baseDirectory
|
||||
_configFile = other._configFile
|
||||
unknownConfigKeysPolicy= other.unknownConfigKeysPolicy
|
||||
devMode = other.devMode
|
||||
}
|
||||
}
|
||||
|
||||
class InitialRegistrationCmdLineOptions : SharedNodeCmdLineOptions() {
|
||||
override fun loadConfig(): NodeConfiguration {
|
||||
return getRawConfig().parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
require(!config.devMode) { "Registration cannot occur in development mode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class NodeCmdLineOptions : SharedNodeCmdLineOptions() {
|
||||
@Option(
|
||||
names = ["--sshd"],
|
||||
description = ["If set, enables SSH server for node administration."]
|
||||
@ -45,90 +92,66 @@ class NodeCmdLineOptions {
|
||||
)
|
||||
var noLocalShell: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--initial-registration"],
|
||||
description = ["Start initial node registration with Corda network to obtain certificate from the permissioning server."]
|
||||
)
|
||||
var isRegistration: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-t", "--network-root-truststore"],
|
||||
description = ["Network root trust store obtained from network operator."]
|
||||
)
|
||||
private var _networkRootTrustStorePath: Path? = null
|
||||
val networkRootTrustStorePath: Path get() = _networkRootTrustStorePath ?: baseDirectory / "certificates" / "network-root-truststore.jks"
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--network-root-truststore-password"],
|
||||
description = ["Network root trust store password obtained from network operator."]
|
||||
)
|
||||
var networkRootTrustStorePassword: String? = null
|
||||
|
||||
@Option(
|
||||
names = ["--on-unknown-config-keys"],
|
||||
description = ["How to behave on unknown node configuration. \${COMPLETION-CANDIDATES}"]
|
||||
)
|
||||
var unknownConfigKeysPolicy: UnknownConfigKeysPolicy = UnknownConfigKeysPolicy.FAIL
|
||||
|
||||
@Option(
|
||||
names = ["-d", "--dev-mode"],
|
||||
description = ["Run the node in developer mode. Unsafe for production."]
|
||||
)
|
||||
var devMode: Boolean? = null
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-node-info"],
|
||||
description = ["Perform the node start-up task necessary to generate its node info, save it to disk, then quit"]
|
||||
description = ["DEPRECATED. Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits."],
|
||||
hidden = true
|
||||
)
|
||||
var justGenerateNodeInfo: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--just-generate-rpc-ssl-settings"],
|
||||
description = ["Generate the SSL key and trust stores for a secure RPC connection."]
|
||||
description = ["DEPRECATED. Generates the SSL key and trust stores for a secure RPC connection."],
|
||||
hidden = true
|
||||
)
|
||||
var justGenerateRpcSslCerts: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["--bootstrap-raft-cluster"],
|
||||
description = ["Bootstraps Raft cluster. The node forms a single node cluster (ignoring otherwise configured peer addresses), acting as a seed for other nodes to join the cluster."]
|
||||
)
|
||||
var bootstrapRaftCluster: Boolean = false
|
||||
|
||||
@Option(
|
||||
names = ["-c", "--clear-network-map-cache"],
|
||||
description = ["Clears local copy of network map, on node startup it will be restored from server or file system."]
|
||||
names = ["--clear-network-map-cache"],
|
||||
description = ["DEPRECATED. Clears local copy of network map, on node startup it will be restored from server or file system."],
|
||||
hidden = true
|
||||
)
|
||||
var clearNetworkMapCache: Boolean = false
|
||||
|
||||
val nodeRegistrationOption: NodeRegistrationOption? by lazy {
|
||||
if (isRegistration) {
|
||||
requireNotNull(networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
@Option(
|
||||
names = ["--initial-registration"],
|
||||
description = ["DEPRECATED. Starts initial node registration with Corda network to obtain certificate from the permissioning server."],
|
||||
hidden = true
|
||||
)
|
||||
var isRegistration: Boolean = false
|
||||
|
||||
fun loadConfig(): Pair<Config, Try<NodeConfiguration>> {
|
||||
@Option(
|
||||
names = ["-t", "--network-root-truststore"],
|
||||
description = ["DEPRECATED. Network root trust store obtained from network operator."],
|
||||
hidden = true
|
||||
)
|
||||
var networkRootTrustStorePathParameter: Path? = null
|
||||
|
||||
@Option(
|
||||
names = ["-p", "--network-root-truststore-password"],
|
||||
description = ["DEPRECATED. Network root trust store password obtained from network operator."],
|
||||
hidden = true
|
||||
)
|
||||
var networkRootTrustStorePassword: String? = null
|
||||
|
||||
override fun loadConfig(): NodeConfiguration {
|
||||
val rawConfig = ConfigHelper.loadConfig(
|
||||
baseDirectory,
|
||||
configFile,
|
||||
configOverrides = ConfigFactory.parseMap(mapOf("noLocalShell" to this.noLocalShell) +
|
||||
if (sshdServer) mapOf("sshd" to mapOf("port" to sshdServerPort.toString())) else emptyMap<String, Any>() +
|
||||
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
|
||||
if (devMode != null) mapOf("devMode" to this.devMode) else emptyMap())
|
||||
)
|
||||
return rawConfig to Try.on {
|
||||
rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (nodeRegistrationOption != null) {
|
||||
require(!config.devMode) { "Registration cannot occur in devMode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
return rawConfig.parseAsNodeConfiguration(unknownConfigKeysPolicy::handle).also { config ->
|
||||
if (isRegistration) {
|
||||
require(!config.devMode) { "Registration cannot occur in development mode" }
|
||||
require(config.compatibilityZoneURL != null || config.networkServices != null) {
|
||||
"compatibilityZoneURL or networkServices must be present in the node configuration file in registration mode."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data class NodeRegistrationOption(val networkRootTrustStorePath: Path, val networkRootTrustStorePassword: String)
|
||||
|
@ -1,6 +1,6 @@
|
||||
package net.corda.node
|
||||
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.core.internal.PLATFORM_VERSION
|
||||
|
||||
/**
|
||||
* Encapsulates various pieces of version information of the node.
|
||||
|
@ -17,6 +17,7 @@ import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.concurrent.map
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
@ -29,14 +30,16 @@ import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationWhitelist
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.days
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.minutes
|
||||
import net.corda.node.CordaClock
|
||||
import net.corda.node.SerialFilter
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.internal.cordapp.CordappConfigFileProvider
|
||||
import net.corda.node.internal.cordapp.CordappProviderImpl
|
||||
import net.corda.node.internal.cordapp.CordappProviderInternal
|
||||
import net.corda.node.internal.cordapp.*
|
||||
import net.corda.node.internal.rpc.proxies.AuthenticatedRpcOpsProxy
|
||||
import net.corda.node.internal.rpc.proxies.ExceptionMaskingRpcOpsProxy
|
||||
import net.corda.node.internal.rpc.proxies.ExceptionSerialisingRpcOpsProxy
|
||||
@ -44,9 +47,12 @@ import net.corda.node.services.ContractUpgradeHandler
|
||||
import net.corda.node.services.FinalityHandler
|
||||
import net.corda.node.services.NotaryChangeHandler
|
||||
import net.corda.node.services.api.*
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NotaryConfig
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.node.services.config.shell.toShellConfig
|
||||
import net.corda.node.services.config.shouldInitCrashShell
|
||||
import net.corda.node.services.events.NodeSchedulerService
|
||||
import net.corda.node.services.events.ScheduledActivityObserver
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
@ -61,10 +67,12 @@ import net.corda.node.services.network.PersistentNetworkMapCache
|
||||
import net.corda.node.services.persistence.*
|
||||
import net.corda.node.services.schema.NodeSchemaService
|
||||
import net.corda.node.services.statemachine.*
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.services.transactions.InMemoryTransactionVerifierService
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.DEV_CERTIFICATES
|
||||
import net.corda.nodeapi.internal.NodeInfoAndSigned
|
||||
import net.corda.nodeapi.internal.SignedNodeInfo
|
||||
import net.corda.nodeapi.internal.config.CertificateStore
|
||||
@ -83,6 +91,7 @@ import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import java.io.IOException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
@ -94,13 +103,10 @@ import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit.MINUTES
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import kotlin.collections.set
|
||||
import kotlin.reflect.KClass
|
||||
import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
|
||||
/**
|
||||
@ -113,14 +119,13 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
// TODO Log warning if this node is a notary but not one of the ones specified in the network parameters, both for core and custom
|
||||
abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val platformClock: CordaClock,
|
||||
cacheFactoryPrototype: NamedCacheFactory,
|
||||
cacheFactoryPrototype: BindableNamedCacheFactory,
|
||||
protected val versionInfo: VersionInfo,
|
||||
protected val cordappLoader: CordappLoader,
|
||||
protected val flowManager: FlowManager,
|
||||
protected val serverThread: AffinityExecutor.ServiceAffinityExecutor,
|
||||
private val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||
|
||||
protected abstract val log: Logger
|
||||
|
||||
@Suppress("LeakingThis")
|
||||
private var tokenizableServices: MutableList<Any>? = mutableListOf(platformClock, this)
|
||||
|
||||
@ -141,15 +146,17 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas, configuration.notary != null).tokenize()
|
||||
protected val cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo)
|
||||
val schemaService = NodeSchemaService(cordappLoader.cordappSchemas).tokenize()
|
||||
val identityService = PersistentIdentityService(cacheFactory).tokenize()
|
||||
val database: CordaPersistence = createCordaPersistence(
|
||||
configuration.database,
|
||||
identityService::wellKnownPartyFromX500Name,
|
||||
identityService::wellKnownPartyFromAnonymous,
|
||||
schemaService,
|
||||
configuration.dataSourceProperties
|
||||
)
|
||||
configuration.dataSourceProperties,
|
||||
cacheFactory)
|
||||
|
||||
init {
|
||||
// TODO Break cyclic dependency
|
||||
identityService.database = database
|
||||
@ -161,7 +168,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val transactionStorage = makeTransactionStorage(configuration.transactionCacheSizeBytes).tokenize()
|
||||
val networkMapClient: NetworkMapClient? = configuration.networkServices?.let { NetworkMapClient(it.networkMapURL, versionInfo) }
|
||||
val attachments = NodeAttachmentService(metricRegistry, cacheFactory, database).tokenize()
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(), attachments).tokenize()
|
||||
val cordappProvider = CordappProviderImpl(cordappLoader, CordappConfigFileProvider(configuration.cordappDirectories), attachments).tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
val keyManagementService = makeKeyManagementService(identityService).tokenize()
|
||||
val servicesForResolution = ServicesForResolutionImpl(identityService, attachments, cordappProvider, transactionStorage).also {
|
||||
@ -169,7 +176,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
@Suppress("LeakingThis")
|
||||
val vaultService = makeVaultService(keyManagementService, servicesForResolution, database).tokenize()
|
||||
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database)
|
||||
val nodeProperties = NodePropertiesPersistentStore(StubbedNodeUniqueIdProvider::value, database, cacheFactory)
|
||||
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
|
||||
val networkMapUpdater = NetworkMapUpdater(
|
||||
networkMapCache,
|
||||
@ -185,7 +192,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
).closeOnStop()
|
||||
@Suppress("LeakingThis")
|
||||
val transactionVerifierService = InMemoryTransactionVerifierService(transactionVerifierWorkerCount).tokenize()
|
||||
val contractUpgradeService = ContractUpgradeServiceImpl().tokenize()
|
||||
val contractUpgradeService = ContractUpgradeServiceImpl(cacheFactory).tokenize()
|
||||
val auditService = DummyAuditService().tokenize()
|
||||
@Suppress("LeakingThis")
|
||||
protected val network: MessagingService = makeMessagingService().tokenize()
|
||||
@ -205,7 +212,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
).tokenize().closeOnStop()
|
||||
|
||||
private val cordappServices = MutableClassToInstanceMap.create<SerializeAsToken>()
|
||||
private val flowFactories = ConcurrentHashMap<Class<out FlowLogic<*>>, InitiatedFlowFactory<*>>()
|
||||
private val shutdownExecutor = Executors.newSingleThreadExecutor()
|
||||
|
||||
protected abstract val transactionVerifierWorkerCount: Int
|
||||
@ -231,7 +237,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
private var _started: S? = null
|
||||
|
||||
private fun <T : Any> T.tokenize(): T {
|
||||
tokenizableServices?.add(this) ?: throw IllegalStateException("The tokenisable services list has already been finalised")
|
||||
tokenizableServices?.add(this)
|
||||
?: throw IllegalStateException("The tokenisable services list has already been finalised")
|
||||
return this
|
||||
}
|
||||
|
||||
@ -264,7 +271,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
check(started == null) { "Node has already been started" }
|
||||
log.info("Generating nodeInfo ...")
|
||||
val trustRoot = initKeyStores()
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
val (identity, identityKeyPair) = obtainIdentity()
|
||||
startDatabase()
|
||||
val nodeCa = configuration.signingCertificateStore.get()[CORDA_CLIENT_CA]
|
||||
identityService.start(trustRoot, listOf(identity.certificate, nodeCa))
|
||||
@ -319,7 +326,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
networkMapCache.start(netParams.notaries)
|
||||
|
||||
startDatabase()
|
||||
val (identity, identityKeyPair) = obtainIdentity(notaryConfig = null)
|
||||
val (identity, identityKeyPair) = obtainIdentity()
|
||||
identityService.start(trustRoot, listOf(identity.certificate, nodeCa))
|
||||
|
||||
val (keyPairs, nodeInfoAndSigned, myNotaryIdentity) = database.transaction {
|
||||
@ -396,8 +403,8 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val keyPairs = mutableSetOf(identityKeyPair)
|
||||
|
||||
val myNotaryIdentity = configuration.notary?.let {
|
||||
if (it.isClusterConfig) {
|
||||
val (notaryIdentity, notaryIdentityKeyPair) = obtainIdentity(it)
|
||||
if (it.serviceLegalName != null) {
|
||||
val (notaryIdentity, notaryIdentityKeyPair) = loadNotaryClusterIdentity(it.serviceLegalName)
|
||||
keyPairs += notaryIdentityKeyPair
|
||||
notaryIdentity
|
||||
} else {
|
||||
@ -500,13 +507,33 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
)
|
||||
}
|
||||
|
||||
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
|
||||
val generatedCordapps = mutableListOf(VirtualCordapp.generateCoreCordapp(versionInfo))
|
||||
if (isRunningSimpleNotaryService(configuration)) {
|
||||
// For backwards compatibility purposes the single node notary implementation is built-in: a virtual
|
||||
// CorDapp will be generated.
|
||||
generatedCordapps += VirtualCordapp.generateSimpleNotaryCordapp(versionInfo)
|
||||
}
|
||||
val blacklistedCerts = if (configuration.devMode) emptyList() else DEV_CERTIFICATES
|
||||
return JarScanningCordappLoader.fromDirectories(
|
||||
configuration.cordappDirectories,
|
||||
versionInfo,
|
||||
extraCordapps = generatedCordapps,
|
||||
blacklistedCerts = blacklistedCerts
|
||||
)
|
||||
}
|
||||
|
||||
private fun isRunningSimpleNotaryService(configuration: NodeConfiguration): Boolean {
|
||||
return configuration.notary != null && configuration.notary?.className == SimpleNotaryService::class.java.name
|
||||
}
|
||||
|
||||
private class ServiceInstantiationException(cause: Throwable?) : CordaException("Service Instantiation Error", cause)
|
||||
|
||||
private fun installCordaServices(myNotaryIdentity: PartyAndCertificate?) {
|
||||
val loadedServices = cordappLoader.cordapps.flatMap { it.services }
|
||||
filterServicesToInstall(loadedServices).forEach {
|
||||
loadedServices.forEach {
|
||||
try {
|
||||
installCordaService(flowStarter, it, myNotaryIdentity)
|
||||
installCordaService(flowStarter, it)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
log.error("${it.name}, as a Corda service, must have a constructor with a single parameter of type " +
|
||||
ServiceHub::class.java.name)
|
||||
@ -518,24 +545,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterServicesToInstall(loadedServices: List<Class<out SerializeAsToken>>): List<Class<out SerializeAsToken>> {
|
||||
val customNotaryServiceList = loadedServices.filter { isNotaryService(it) }
|
||||
if (customNotaryServiceList.isNotEmpty()) {
|
||||
if (configuration.notary?.custom == true) {
|
||||
require(customNotaryServiceList.size == 1) {
|
||||
"Attempting to install more than one notary service: ${customNotaryServiceList.joinToString()}"
|
||||
}
|
||||
} else return loadedServices - customNotaryServiceList
|
||||
}
|
||||
return loadedServices
|
||||
}
|
||||
|
||||
/**
|
||||
* If the [serviceClass] is a notary service, it will only be enabled if the "custom" flag is set in
|
||||
* the notary configuration.
|
||||
*/
|
||||
private fun isNotaryService(serviceClass: Class<*>) = NotaryService::class.java.isAssignableFrom(serviceClass)
|
||||
|
||||
/**
|
||||
* This customizes the ServiceHub for each CordaService that is initiating flows.
|
||||
*/
|
||||
@ -576,139 +585,52 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
override fun hashCode() = Objects.hash(serviceHub, flowStarter, serviceInstance)
|
||||
}
|
||||
|
||||
private fun <T : SerializeAsToken> installCordaService(flowStarter: FlowStarter, serviceClass: Class<T>, myNotaryIdentity: PartyAndCertificate?) {
|
||||
private fun <T : SerializeAsToken> installCordaService(flowStarter: FlowStarter, serviceClass: Class<T>) {
|
||||
serviceClass.requireAnnotation<CordaService>()
|
||||
|
||||
val service = try {
|
||||
if (isNotaryService(serviceClass)) {
|
||||
myNotaryIdentity ?: throw IllegalStateException("Trying to install a notary service but no notary identity specified")
|
||||
try {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
constructor.newInstance(services, myNotaryIdentity.owningKey )
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
val service = constructor.newInstance(serviceContext, myNotaryIdentity.owningKey)
|
||||
serviceContext.serviceInstance = service
|
||||
service
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
|
||||
val service = extendedServiceConstructor.newInstance(serviceContext)
|
||||
serviceContext.serviceInstance = service
|
||||
service
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true }
|
||||
log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " +
|
||||
"Upgrade to an AppServiceHub parameter to enable updated API features.")
|
||||
constructor.newInstance(services)
|
||||
}
|
||||
}
|
||||
val serviceContext = AppServiceHubImpl<T>(services, flowStarter)
|
||||
val extendedServiceConstructor = serviceClass.getDeclaredConstructor(AppServiceHub::class.java).apply { isAccessible = true }
|
||||
val service = extendedServiceConstructor.newInstance(serviceContext)
|
||||
serviceContext.serviceInstance = service
|
||||
service
|
||||
} catch (ex: NoSuchMethodException) {
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHub::class.java).apply { isAccessible = true }
|
||||
log.warn("${serviceClass.name} is using legacy CordaService constructor with ServiceHub parameter. " +
|
||||
"Upgrade to an AppServiceHub parameter to enable updated API features.")
|
||||
constructor.newInstance(services)
|
||||
} catch (e: InvocationTargetException) {
|
||||
throw ServiceInstantiationException(e.cause)
|
||||
}
|
||||
|
||||
cordappServices.putInstance(serviceClass, service)
|
||||
|
||||
if (service is NotaryService) handleCustomNotaryService(service)
|
||||
service.tokenize()
|
||||
log.info("Installed ${serviceClass.name} Corda service")
|
||||
}
|
||||
|
||||
private fun handleCustomNotaryService(service: NotaryService) {
|
||||
runOnStop += service::stop
|
||||
installCoreFlow(NotaryFlow.Client::class, service::createServiceFlow)
|
||||
service.start()
|
||||
}
|
||||
|
||||
private fun registerCordappFlows() {
|
||||
cordappLoader.cordapps.flatMap { it.initiatedFlows }
|
||||
.forEach {
|
||||
cordappLoader.cordapps.forEach { cordapp ->
|
||||
cordapp.initiatedFlows.groupBy { it.requireAnnotation<InitiatedBy>().value.java }.forEach { initiator, responders ->
|
||||
responders.forEach { responder ->
|
||||
try {
|
||||
registerInitiatedFlowInternal(smm, it, track = false)
|
||||
flowManager.registerInitiatedFlow(initiator, responder)
|
||||
} catch (e: NoSuchMethodException) {
|
||||
log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " +
|
||||
log.error("${responder.name}, as an initiated flow, must have a constructor with a single parameter " +
|
||||
"of type ${Party::class.java.name}")
|
||||
} catch (e: Exception) {
|
||||
log.error("Unable to register initiated flow ${it.name}", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>): Observable<T> {
|
||||
return registerInitiatedFlowInternal(smm, initiatedFlowClass, track = true)
|
||||
}
|
||||
|
||||
// TODO remove once not needed
|
||||
private fun deprecatedFlowConstructorMessage(flowClass: Class<*>): String {
|
||||
return "Installing flow factory for $flowClass accepting a ${Party::class.java.simpleName}, which is deprecated. " +
|
||||
"It should accept a ${FlowSession::class.java.simpleName} instead"
|
||||
}
|
||||
|
||||
private fun <F : FlowLogic<*>> registerInitiatedFlowInternal(smm: StateMachineManager, initiatedFlow: Class<F>, track: Boolean): Observable<F> {
|
||||
val constructors = initiatedFlow.declaredConstructors.associateBy { it.parameterTypes.toList() }
|
||||
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
|
||||
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
|
||||
// Try to fallback to a Party constructor
|
||||
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
|
||||
if (partyCtor == null) {
|
||||
throw IllegalArgumentException("$initiatedFlow must have a constructor accepting a ${FlowSession::class.java.name}")
|
||||
} else {
|
||||
log.warn(deprecatedFlowConstructorMessage(initiatedFlow))
|
||||
}
|
||||
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
|
||||
} else {
|
||||
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
|
||||
}
|
||||
val initiatingFlow = initiatedFlow.requireAnnotation<InitiatedBy>().value.java
|
||||
val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass
|
||||
require(classWithAnnotation == initiatingFlow) {
|
||||
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiatingFlow.name}"
|
||||
}
|
||||
val flowFactory = InitiatedFlowFactory.CorDapp(version, initiatedFlow.appName, ctor)
|
||||
val observable = internalRegisterFlowFactory(smm, initiatingFlow, flowFactory, initiatedFlow, track)
|
||||
log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)")
|
||||
return observable
|
||||
}
|
||||
|
||||
protected fun <F : FlowLogic<*>> internalRegisterFlowFactory(smm: StateMachineManager,
|
||||
initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>,
|
||||
track: Boolean): Observable<F> {
|
||||
val observable = if (track) {
|
||||
smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass)
|
||||
} else {
|
||||
Observable.empty()
|
||||
}
|
||||
check(initiatingFlowClass !in flowFactories.keys) {
|
||||
"$initiatingFlowClass is attempting to register multiple initiated flows"
|
||||
}
|
||||
flowFactories[initiatingFlowClass] = flowFactory
|
||||
return observable
|
||||
}
|
||||
|
||||
/**
|
||||
* Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using
|
||||
* [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards
|
||||
* compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party.
|
||||
*/
|
||||
@VisibleForTesting
|
||||
fun installCoreFlow(clientFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||
}
|
||||
flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory)
|
||||
log.debug { "Installed core flow ${clientFlowClass.java.name}" }
|
||||
flowManager.validateRegistrations()
|
||||
}
|
||||
|
||||
private fun installCoreFlows() {
|
||||
installCoreFlow(FinalityFlow::class, ::FinalityHandler)
|
||||
installCoreFlow(NotaryChangeFlow::class, ::NotaryChangeHandler)
|
||||
installCoreFlow(ContractUpgradeFlow.Initiate::class, ::ContractUpgradeHandler)
|
||||
installCoreFlow(SwapIdentitiesFlow::class, ::SwapIdentitiesHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(FinalityFlow::class, FinalityHandler::class, ::FinalityHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryChangeFlow::class, NotaryChangeHandler::class, ::NotaryChangeHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(ContractUpgradeFlow.Initiate::class, NotaryChangeHandler::class, ::ContractUpgradeHandler)
|
||||
flowManager.registerInitiatedCoreFlowFactory(SwapIdentitiesFlow::class, SwapIdentitiesHandler::class, ::SwapIdentitiesHandler)
|
||||
}
|
||||
|
||||
protected open fun makeTransactionStorage(transactionCacheSizeBytes: Long): WritableTransactionStorage {
|
||||
@ -725,9 +647,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
val identitiesKeyStore = configuration.signingCertificateStore.get()
|
||||
val trustStore = configuration.p2pSslOptions.trustStore.get()
|
||||
AllCertificateStores(trustStore, sslKeyStore, identitiesKeyStore)
|
||||
} catch (e: KeyStoreException) {
|
||||
log.warn("At least one of the keystores or truststore passwords does not match configuration.")
|
||||
null
|
||||
} catch (e: IOException) {
|
||||
log.error("IO exception while trying to validate keystores and truststore", e)
|
||||
null
|
||||
@ -738,11 +657,15 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
|
||||
private fun validateKeyStores(): X509Certificate {
|
||||
// Step 1. Check trustStore, sslKeyStore and identitiesKeyStore exist.
|
||||
val certStores = requireNotNull(getCertificateStores()) {
|
||||
"One or more keyStores (identity or TLS) or trustStore not found. " +
|
||||
"Please either copy your existing keys and certificates from another node, " +
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar --initial-registration. " +
|
||||
"Read more at: https://docs.corda.net/permissioning.html"
|
||||
val certStores = try {
|
||||
requireNotNull(getCertificateStores()) {
|
||||
"One or more keyStores (identity or TLS) or trustStore not found. " +
|
||||
"Please either copy your existing keys and certificates from another node, " +
|
||||
"or if you don't have one yet, fill out the config file and run corda.jar initial-registration. " +
|
||||
"Read more at: https://docs.corda.net/permissioning.html"
|
||||
}
|
||||
} catch (e: KeyStoreException) {
|
||||
throw IllegalArgumentException("At least one of the keystores or truststore passwords does not match configuration.")
|
||||
}
|
||||
// Step 2. Check that trustStore contains the correct key-alias entry.
|
||||
require(CORDA_ROOT_CA in certStores.trustStore) {
|
||||
@ -781,17 +704,53 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun makeNotaryService(myNotaryIdentity: PartyAndCertificate?): NotaryService? {
|
||||
return configuration.notary?.let {
|
||||
makeCoreNotaryService(it, myNotaryIdentity).also {
|
||||
it.tokenize()
|
||||
runOnStop += it::stop
|
||||
installCoreFlow(NotaryFlow.Client::class, it::createServiceFlow)
|
||||
log.info("Running core notary: ${it.javaClass.name}")
|
||||
it.start()
|
||||
return configuration.notary?.let { notaryConfig ->
|
||||
val serviceClass = getNotaryServiceClass(notaryConfig.className)
|
||||
log.info("Starting notary service: $serviceClass")
|
||||
|
||||
val notaryKey = myNotaryIdentity?.owningKey
|
||||
?: throw IllegalArgumentException("Unable to start notary service $serviceClass: notary identity not found")
|
||||
|
||||
/** Some notary implementations only work with Java serialization. */
|
||||
maybeInstallSerializationFilter(serviceClass)
|
||||
|
||||
val constructor = serviceClass.getDeclaredConstructor(ServiceHubInternal::class.java, PublicKey::class.java).apply { isAccessible = true }
|
||||
val service = constructor.newInstance(services, notaryKey) as NotaryService
|
||||
|
||||
service.run {
|
||||
tokenize()
|
||||
runOnStop += ::stop
|
||||
flowManager.registerInitiatedCoreFlowFactory(NotaryFlow.Client::class, ::createServiceFlow)
|
||||
start()
|
||||
}
|
||||
return service
|
||||
}
|
||||
}
|
||||
|
||||
/** Installs a custom serialization filter defined by a notary service implementation. Only supported in dev mode. */
|
||||
private fun maybeInstallSerializationFilter(serviceClass: Class<out NotaryService>) {
|
||||
try {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val filter = serviceClass.getDeclaredMethod("getSerializationFilter").invoke(null) as ((Class<*>) -> Boolean)
|
||||
if (configuration.devMode) {
|
||||
log.warn("Installing a custom Java serialization filter, required by ${serviceClass.name}. " +
|
||||
"Note this is only supported in dev mode – a production node will fail to start if serialization filters are used.")
|
||||
SerialFilter.install(filter)
|
||||
} else {
|
||||
throw UnsupportedOperationException("Unable to install a custom Java serialization filter, not in dev mode.")
|
||||
}
|
||||
} catch (e: NoSuchMethodException) {
|
||||
// No custom serialization filter declared
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNotaryServiceClass(className: String): Class<out NotaryService> {
|
||||
val loadedImplementations = cordappLoader.cordapps.mapNotNull { it.notaryService }
|
||||
log.debug("Notary service implementations found: ${loadedImplementations.joinToString(", ")}")
|
||||
return loadedImplementations.firstOrNull { it.name == className }
|
||||
?: throw IllegalArgumentException("The notary service implementation specified in the configuration: $className is not found. Available implementations: ${loadedImplementations.joinToString(", ")}}")
|
||||
}
|
||||
|
||||
protected open fun makeKeyManagementService(identityService: PersistentIdentityService): KeyManagementServiceInternal {
|
||||
// Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because
|
||||
// the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with
|
||||
@ -799,32 +758,6 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
return PersistentKeyManagementService(cacheFactory, identityService, database)
|
||||
}
|
||||
|
||||
private fun makeCoreNotaryService(notaryConfig: NotaryConfig, myNotaryIdentity: PartyAndCertificate?): NotaryService {
|
||||
val notaryKey = myNotaryIdentity?.owningKey
|
||||
?: throw IllegalArgumentException("No notary identity initialized when creating a notary service")
|
||||
return notaryConfig.run {
|
||||
when {
|
||||
raft != null -> {
|
||||
val uniquenessProvider = RaftUniquenessProvider(configuration.baseDirectory, configuration.p2pSslOptions, database, platformClock, monitoringService.metrics, cacheFactory, raft)
|
||||
(if (validating) ::RaftValidatingNotaryService else ::RaftNonValidatingNotaryService)(services, notaryKey, uniquenessProvider)
|
||||
}
|
||||
bftSMaRt != null -> {
|
||||
if (validating) throw IllegalArgumentException("Validating BFTSMaRt notary not supported")
|
||||
BFTNonValidatingNotaryService(services, notaryKey, bftSMaRt, makeBFTCluster(notaryKey, bftSMaRt))
|
||||
}
|
||||
else -> (if (validating) ::ValidatingNotaryService else ::SimpleNotaryService)(services, notaryKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun makeBFTCluster(notaryKey: PublicKey, bftSMaRtConfig: BFTSMaRtConfiguration): BFTSMaRt.Cluster {
|
||||
return object : BFTSMaRt.Cluster {
|
||||
override fun waitUntilAllReplicasHaveInitialized() {
|
||||
log.warn("A BFT replica may still be initializing, in which case the upcoming consensus change may cause it to spin.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun stop() {
|
||||
// TODO: We need a good way of handling "nice to have" shutdown events, especially those that deal with the
|
||||
// network, including unsubscribing from updates from remote services. Possibly some sort of parameter to stop()
|
||||
@ -848,55 +781,56 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
myNotaryIdentity: PartyAndCertificate?,
|
||||
networkParameters: NetworkParameters)
|
||||
|
||||
private fun obtainIdentity(notaryConfig: NotaryConfig?): Pair<PartyAndCertificate, KeyPair> {
|
||||
/** Loads or generates the node's legal identity and key-pair. */
|
||||
private fun obtainIdentity(): Pair<PartyAndCertificate, KeyPair> {
|
||||
val keyStore = configuration.signingCertificateStore.get()
|
||||
val legalName = configuration.myLegalName
|
||||
|
||||
val (id, singleName) = if (notaryConfig == null || !notaryConfig.isClusterConfig) {
|
||||
// Node's main identity or if it's a single node notary.
|
||||
Pair(NODE_IDENTITY_ALIAS_PREFIX, configuration.myLegalName)
|
||||
} else {
|
||||
// The node is part of a distributed notary whose identity must already be generated beforehand.
|
||||
Pair(DISTRIBUTED_NOTARY_ALIAS_PREFIX, null)
|
||||
}
|
||||
// TODO: Integrate with Key management service?
|
||||
val privateKeyAlias = "$id-private-key"
|
||||
|
||||
val privateKeyAlias = "$NODE_IDENTITY_ALIAS_PREFIX-private-key"
|
||||
if (privateKeyAlias !in keyStore) {
|
||||
// We shouldn't have a distributed notary at this stage, so singleName should NOT be null.
|
||||
requireNotNull(singleName) {
|
||||
"Unable to find in the key store the identity of the distributed notary the node is part of"
|
||||
}
|
||||
log.info("$privateKeyAlias not found in key store, generating fresh key!")
|
||||
keyStore.storeLegalIdentity(privateKeyAlias, generateKeyPair())
|
||||
}
|
||||
|
||||
val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias) }
|
||||
val (x509Cert, keyPair) = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) }
|
||||
|
||||
// TODO: Use configuration to indicate composite key should be used instead of public key for the identity.
|
||||
val compositeKeyAlias = "$id-composite-key"
|
||||
val certificates = keyStore.query { getCertificateChain(privateKeyAlias) }
|
||||
check(certificates.first() == x509Cert) {
|
||||
"Certificates from key store do not line up!"
|
||||
}
|
||||
|
||||
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
|
||||
if (subject != legalName) {
|
||||
throw ConfigurationException("The name '$legalName' for $NODE_IDENTITY_ALIAS_PREFIX doesn't match what's in the key store: $subject")
|
||||
}
|
||||
|
||||
val certPath = X509Utilities.buildCertPath(certificates)
|
||||
return Pair(PartyAndCertificate(certPath), keyPair)
|
||||
}
|
||||
|
||||
/** Loads pre-generated notary service cluster identity. */
|
||||
private fun loadNotaryClusterIdentity(serviceLegalName: CordaX500Name): Pair<PartyAndCertificate, KeyPair> {
|
||||
val keyStore = configuration.signingCertificateStore.get()
|
||||
|
||||
val privateKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-private-key"
|
||||
val keyPair = keyStore.query { getCertificateAndKeyPair(privateKeyAlias, keyStore.entryPassword) }.keyPair
|
||||
|
||||
val compositeKeyAlias = "$DISTRIBUTED_NOTARY_ALIAS_PREFIX-composite-key"
|
||||
val certificates = if (compositeKeyAlias in keyStore) {
|
||||
// Use composite key instead if it exists.
|
||||
val certificate = keyStore[compositeKeyAlias]
|
||||
// We have to create the certificate chain for the composite key manually, this is because we don't have a keystore
|
||||
// provider that understand compositeKey-privateKey combo. The cert chain is created using the composite key certificate +
|
||||
// the tail of the private key certificates, as they are both signed by the same certificate chain.
|
||||
listOf(certificate) + keyStore.query { getCertificateChain(privateKeyAlias) }.drop(1)
|
||||
} else {
|
||||
keyStore.query { getCertificateChain(privateKeyAlias) }.let {
|
||||
check(it[0] == x509Cert) { "Certificates from key store do not line up!" }
|
||||
it
|
||||
}
|
||||
}
|
||||
} else throw IllegalStateException("The identity public key for the notary service $serviceLegalName was not found in the key store.")
|
||||
|
||||
val subject = CordaX500Name.build(certificates[0].subjectX500Principal)
|
||||
if (singleName != null && subject != singleName) {
|
||||
throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject")
|
||||
} else if (notaryConfig != null && notaryConfig.isClusterConfig && notaryConfig.serviceLegalName != null && subject != notaryConfig.serviceLegalName) {
|
||||
// Note that we're not checking if `notaryConfig.serviceLegalName` is not present for backwards compatibility.
|
||||
throw ConfigurationException("The name of the notary service '${notaryConfig.serviceLegalName}' for $id doesn't " +
|
||||
val subject = CordaX500Name.build(certificates.first().subjectX500Principal)
|
||||
if (subject != serviceLegalName) {
|
||||
throw ConfigurationException("The name of the notary service '$serviceLegalName' for $DISTRIBUTED_NOTARY_ALIAS_PREFIX doesn't " +
|
||||
"match what's in the key store: $subject. You might need to adjust the configuration of `notary.serviceLegalName`.")
|
||||
}
|
||||
|
||||
val certPath = X509Utilities.buildCertPath(certificates)
|
||||
return Pair(PartyAndCertificate(certPath), keyPair)
|
||||
}
|
||||
@ -966,7 +900,7 @@ abstract class AbstractNode<S>(val configuration: NodeConfiguration,
|
||||
}
|
||||
|
||||
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||
return flowFactories[initiatingFlowClass]
|
||||
return flowManager.getFlowFactoryForInitiatingFlow(initiatingFlowClass)
|
||||
}
|
||||
|
||||
override fun jdbcSession(): Connection = database.createSession()
|
||||
@ -1032,23 +966,12 @@ class FlowStarterImpl(private val smm: StateMachineManager, private val flowLogi
|
||||
|
||||
class ConfigurationException(message: String) : CordaException(message)
|
||||
|
||||
// TODO This is no longer used by AbstractNode and can be moved elsewhere
|
||||
fun configureDatabase(hikariProperties: Properties,
|
||||
databaseConfig: DatabaseConfig,
|
||||
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
||||
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
||||
schemaService: SchemaService = NodeSchemaService(),
|
||||
internalSchemas: Set<MappedSchema> = NodeSchemaService().internalSchemas()): CordaPersistence {
|
||||
val persistence = createCordaPersistence(databaseConfig, wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous, schemaService, hikariProperties)
|
||||
persistence.startHikariPool(hikariProperties, databaseConfig, internalSchemas)
|
||||
return persistence
|
||||
}
|
||||
|
||||
fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
wellKnownPartyFromX500Name: (CordaX500Name) -> Party?,
|
||||
wellKnownPartyFromAnonymous: (AbstractParty) -> Party?,
|
||||
schemaService: SchemaService,
|
||||
hikariProperties: Properties): CordaPersistence {
|
||||
hikariProperties: Properties,
|
||||
cacheFactory: NamedCacheFactory): CordaPersistence {
|
||||
// Register the AbstractPartyDescriptor so Hibernate doesn't warn when encountering AbstractParty. Unfortunately
|
||||
// Hibernate warns about not being able to find a descriptor if we don't provide one, but won't use it by default
|
||||
// so we end up providing both descriptor and converter. We should re-examine this in later versions to see if
|
||||
@ -1056,7 +979,7 @@ fun createCordaPersistence(databaseConfig: DatabaseConfig,
|
||||
JavaTypeDescriptorRegistry.INSTANCE.addDescriptor(AbstractPartyDescriptor(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
val attributeConverters = listOf(PublicKeyToTextConverter(), AbstractPartyToX500NameAsStringConverter(wellKnownPartyFromX500Name, wellKnownPartyFromAnonymous))
|
||||
val jdbcUrl = hikariProperties.getProperty("dataSource.url", "")
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, attributeConverters)
|
||||
return CordaPersistence(databaseConfig, schemaService.schemaOptions.keys, jdbcUrl, cacheFactory, attributeConverters)
|
||||
}
|
||||
|
||||
fun CordaPersistence.startHikariPool(hikariProperties: Properties, databaseConfig: DatabaseConfig, schemas: Set<MappedSchema>, metricRegistry: MetricRegistry? = null) {
|
||||
@ -1082,4 +1005,4 @@ fun clientSslOptionsCompatibleWith(nodeRpcOptions: NodeRpcOptions): ClientRpcSsl
|
||||
}
|
||||
// Here we're using the node's RPC key store as the RPC client's trust store.
|
||||
return ClientRpcSslOptions(trustStorePath = nodeRpcOptions.sslConfig!!.keyStorePath, trustStorePassword = nodeRpcOptions.sslConfig!!.keyStorePassword)
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
|
||||
import net.corda.core.serialization.internal.checkpointDeserialize
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
@ -21,7 +20,7 @@ object CheckpointVerifier {
|
||||
*/
|
||||
fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any>) {
|
||||
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
)
|
||||
checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) ->
|
||||
|
||||
|
222
node/src/main/kotlin/net/corda/node/internal/FlowManager.kt
Normal file
222
node/src/main/kotlin/net/corda/node/internal/FlowManager.kt
Normal file
@ -0,0 +1,222 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.node.services.config.FlowOverrideConfig
|
||||
import net.corda.node.services.statemachine.appName
|
||||
import net.corda.node.services.statemachine.flowVersionAndInitiatingClass
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
*
|
||||
* This class is responsible for organising which flow should respond to a specific @InitiatingFlow
|
||||
*
|
||||
* There are two main ways to modify the behaviour of a cordapp with regards to responding with a different flow
|
||||
*
|
||||
* 1.) implementing a new subclass. For example, if we have a ResponderFlow similar to @InitiatedBy(Sender) MyBaseResponder : FlowLogic
|
||||
* If we subclassed a new Flow with specific logic for DB2, it would be similar to IBMB2Responder() : MyBaseResponder
|
||||
* When these two flows are encountered by the classpath scan for @InitiatedBy, they will both be selected for responding to Sender
|
||||
* This implementation will sort them for responding in order of their "depth" from FlowLogic - see: FlowWeightComparator
|
||||
* So IBMB2Responder would win and it would be selected for responding
|
||||
*
|
||||
* 2.) It is possible to specify a flowOverride key in the node configuration. Say we configure a node to have
|
||||
* flowOverrides{
|
||||
* "Sender" = "MyBaseResponder"
|
||||
* }
|
||||
* In this case, FlowWeightComparator would detect that there is an override in action, and it will assign MyBaseResponder a maximum weight
|
||||
* This will result in MyBaseResponder being selected for responding to Sender
|
||||
*
|
||||
*
|
||||
*/
|
||||
interface FlowManager {
|
||||
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>)
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>)
|
||||
fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>)
|
||||
|
||||
fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>)
|
||||
fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>)
|
||||
|
||||
fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>?
|
||||
|
||||
fun validateRegistrations()
|
||||
}
|
||||
|
||||
@ThreadSafe
|
||||
open class NodeFlowManager(flowOverrides: FlowOverrideConfig? = null) : FlowManager {
|
||||
|
||||
private val flowFactories = HashMap<Class<out FlowLogic<*>>, MutableList<RegisteredFlowContainer>>()
|
||||
private val flowOverrides = (flowOverrides
|
||||
?: FlowOverrideConfig()).overrides.map { it.initiator to it.responder }.toMutableMap()
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun getFlowFactoryForInitiatingFlow(initiatedFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {
|
||||
return flowFactories[initiatedFlowClass]?.firstOrNull()?.flowFactory
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun <F : FlowLogic<*>> registerInitiatedFlow(responder: Class<F>) {
|
||||
return registerInitiatedFlow(responder.requireAnnotation<InitiatedBy>().value.java, responder)
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun <F : FlowLogic<*>> registerInitiatedFlow(initiator: Class<out FlowLogic<*>>, responder: Class<F>) {
|
||||
val constructors = responder.declaredConstructors.associateBy { it.parameterTypes.toList() }
|
||||
val flowSessionCtor = constructors[listOf(FlowSession::class.java)]?.apply { isAccessible = true }
|
||||
val ctor: (FlowSession) -> F = if (flowSessionCtor == null) {
|
||||
// Try to fallback to a Party constructor
|
||||
val partyCtor = constructors[listOf(Party::class.java)]?.apply { isAccessible = true }
|
||||
if (partyCtor == null) {
|
||||
throw IllegalArgumentException("$responder must have a constructor accepting a ${FlowSession::class.java.name}")
|
||||
} else {
|
||||
log.warn("Installing flow factory for $responder accepting a ${Party::class.java.simpleName}, which is deprecated. " +
|
||||
"It should accept a ${FlowSession::class.java.simpleName} instead")
|
||||
}
|
||||
{ flowSession: FlowSession -> uncheckedCast(partyCtor.newInstance(flowSession.counterparty)) }
|
||||
} else {
|
||||
{ flowSession: FlowSession -> uncheckedCast(flowSessionCtor.newInstance(flowSession)) }
|
||||
}
|
||||
val (version, classWithAnnotation) = initiator.flowVersionAndInitiatingClass
|
||||
require(classWithAnnotation == initiator) {
|
||||
"${InitiatedBy::class.java.name} must point to ${classWithAnnotation.name} and not ${initiator.name}"
|
||||
}
|
||||
val flowFactory = InitiatedFlowFactory.CorDapp(version, responder.appName, ctor)
|
||||
registerInitiatedFlowFactory(initiator, flowFactory, responder)
|
||||
log.info("Registered ${initiator.name} to initiate ${responder.name} (version $version)")
|
||||
}
|
||||
|
||||
private fun <F : FlowLogic<*>> registerInitiatedFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
flowFactory: InitiatedFlowFactory<F>,
|
||||
initiatedFlowClass: Class<F>?) {
|
||||
|
||||
check(flowFactory !is InitiatedFlowFactory.Core) { "This should only be used for Cordapp flows" }
|
||||
val listOfFlowsForInitiator = flowFactories.computeIfAbsent(initiatingFlowClass) { mutableListOf() }
|
||||
if (listOfFlowsForInitiator.isNotEmpty() && listOfFlowsForInitiator.first().type == FlowType.CORE) {
|
||||
throw IllegalStateException("Attempting to register over an existing platform flow: $initiatingFlowClass")
|
||||
}
|
||||
synchronized(listOfFlowsForInitiator) {
|
||||
val flowToAdd = RegisteredFlowContainer(initiatingFlowClass, initiatedFlowClass, flowFactory, FlowType.CORDAPP)
|
||||
val flowWeightComparator = FlowWeightComparator(initiatingFlowClass, flowOverrides)
|
||||
listOfFlowsForInitiator.add(flowToAdd)
|
||||
listOfFlowsForInitiator.sortWith(flowWeightComparator)
|
||||
if (listOfFlowsForInitiator.size > 1) {
|
||||
log.warn("Multiple flows are registered for InitiatingFlow: $initiatingFlowClass, currently using: ${listOfFlowsForInitiator.first().initiatedFlowClass}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO Harmonise use of these methods - 99% of invocations come from tests.
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
registerInitiatedCoreFlowFactory(initiatingFlowClass, initiatedFlowClass, InitiatedFlowFactory.Core(flowFactory))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, flowFactory: (FlowSession) -> FlowLogic<*>) {
|
||||
registerInitiatedCoreFlowFactory(initiatingFlowClass, null, InitiatedFlowFactory.Core(flowFactory))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun registerInitiatedCoreFlowFactory(initiatingFlowClass: KClass<out FlowLogic<*>>, initiatedFlowClass: KClass<out FlowLogic<*>>?, flowFactory: InitiatedFlowFactory.Core<FlowLogic<*>>) {
|
||||
require(initiatingFlowClass.java.flowVersionAndInitiatingClass.first == 1) {
|
||||
"${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version"
|
||||
}
|
||||
flowFactories.computeIfAbsent(initiatingFlowClass.java) { mutableListOf() }.add(
|
||||
RegisteredFlowContainer(
|
||||
initiatingFlowClass.java,
|
||||
initiatedFlowClass?.java,
|
||||
flowFactory,
|
||||
FlowType.CORE)
|
||||
)
|
||||
log.debug { "Installed core flow ${initiatingFlowClass.java.name}" }
|
||||
}
|
||||
|
||||
// To verify the integrity of the current state, it is important that the tip of the responders is a unique weight
|
||||
// if there are multiple flows with the same weight as the tip, it means that it is impossible to reliably pick one as the responder
|
||||
private fun validateInvariants(toValidate: List<RegisteredFlowContainer>) {
|
||||
val currentTip = toValidate.first()
|
||||
val flowWeightComparator = FlowWeightComparator(currentTip.initiatingFlowClass, flowOverrides)
|
||||
val equalWeightAsCurrentTip = toValidate.map { flowWeightComparator.compare(currentTip, it) to it }.filter { it.first == 0 }.map { it.second }
|
||||
if (equalWeightAsCurrentTip.size > 1) {
|
||||
val message = "Unable to determine which flow to use when responding to: ${currentTip.initiatingFlowClass.canonicalName}. ${equalWeightAsCurrentTip.map { it.initiatedFlowClass!!.canonicalName }} are all registered with equal weight."
|
||||
throw IllegalStateException(message)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
override fun validateRegistrations() {
|
||||
flowFactories.values.forEach {
|
||||
validateInvariants(it)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class FlowType {
|
||||
CORE, CORDAPP
|
||||
}
|
||||
|
||||
private data class RegisteredFlowContainer(val initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
val initiatedFlowClass: Class<out FlowLogic<*>>?,
|
||||
val flowFactory: InitiatedFlowFactory<FlowLogic<*>>,
|
||||
val type: FlowType)
|
||||
|
||||
// this is used to sort the responding flows in order of "importance"
|
||||
// the logic is as follows
|
||||
// IF responder is a specific lambda (like for notary implementations / testing code) always return that responder
|
||||
// ELSE IF responder is present in the overrides list, always return that responder
|
||||
// ELSE compare responding flows by their depth from FlowLogic, always return the flow which is most specific (IE, has the most hops to FlowLogic)
|
||||
private open class FlowWeightComparator(val initiatingFlowClass: Class<out FlowLogic<*>>, val flowOverrides: Map<String, String>) : Comparator<NodeFlowManager.RegisteredFlowContainer> {
|
||||
|
||||
override fun compare(o1: NodeFlowManager.RegisteredFlowContainer, o2: NodeFlowManager.RegisteredFlowContainer): Int {
|
||||
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass != null) {
|
||||
return Int.MIN_VALUE
|
||||
}
|
||||
if (o1.initiatedFlowClass != null && o2.initiatedFlowClass == null) {
|
||||
return Int.MAX_VALUE
|
||||
}
|
||||
|
||||
if (o1.initiatedFlowClass == null && o2.initiatedFlowClass == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
val hopsTo1 = calculateHopsToFlowLogic(initiatingFlowClass, o1.initiatedFlowClass!!)
|
||||
val hopsTo2 = calculateHopsToFlowLogic(initiatingFlowClass, o2.initiatedFlowClass!!)
|
||||
return hopsTo1.compareTo(hopsTo2) * -1
|
||||
}
|
||||
|
||||
private fun calculateHopsToFlowLogic(initiatingFlowClass: Class<out FlowLogic<*>>,
|
||||
initiatedFlowClass: Class<out FlowLogic<*>>): Int {
|
||||
|
||||
val overriddenClassName = flowOverrides[initiatingFlowClass.canonicalName]
|
||||
return if (overriddenClassName == initiatedFlowClass.canonicalName) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
var currentClass: Class<*> = initiatedFlowClass
|
||||
var count = 0
|
||||
while (currentClass != FlowLogic::class.java) {
|
||||
currentClass = currentClass.superclass
|
||||
count++
|
||||
}
|
||||
count;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private fun <X, Y> Iterable<Pair<X, Y>>.toMutableMap(): MutableMap<X, Y> {
|
||||
return this.toMap(HashMap())
|
||||
}
|
@ -4,10 +4,12 @@ import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
|
||||
sealed class InitiatedFlowFactory<out F : FlowLogic<*>> {
|
||||
|
||||
protected abstract val factory: (FlowSession) -> F
|
||||
fun createFlow(initiatingFlowSession: FlowSession): F = factory(initiatingFlowSession)
|
||||
|
||||
data class Core<out F : FlowLogic<*>>(override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
|
||||
|
||||
data class CorDapp<out F : FlowLogic<*>>(val flowVersion: Int,
|
||||
val appName: String,
|
||||
override val factory: (FlowSession) -> F) : InitiatedFlowFactory<F>()
|
||||
|
@ -6,6 +6,7 @@ import com.codahale.metrics.MetricRegistry
|
||||
import com.palominolabs.metrics.newrelic.AllEnabledMetricAttributeFilter
|
||||
import com.palominolabs.metrics.newrelic.NewRelicReporter
|
||||
import net.corda.client.rpc.internal.serialization.amqp.AMQPClientSerializationScheme
|
||||
import net.corda.cliutils.ShellConstants
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
@ -21,24 +22,21 @@ 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.internal.CheckpointSerializationFactory
|
||||
import net.corda.core.serialization.internal.SerializationEnvironmentImpl
|
||||
import net.corda.core.serialization.internal.SerializationEnvironment
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.CordaClock
|
||||
import net.corda.node.SimpleClock
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.artemis.ArtemisBroker
|
||||
import net.corda.node.internal.artemis.BrokerAddresses
|
||||
import net.corda.node.internal.cordapp.JarScanningCordappLoader
|
||||
import net.corda.node.internal.security.RPCSecurityManager
|
||||
import net.corda.node.internal.security.RPCSecurityManagerImpl
|
||||
import net.corda.node.internal.security.RPCSecurityManagerWithAdditionalUser
|
||||
import net.corda.node.serialization.amqp.AMQPServerSerializationScheme
|
||||
import net.corda.node.serialization.kryo.KRYO_CHECKPOINT_CONTEXT
|
||||
import net.corda.node.serialization.kryo.KryoSerializationScheme
|
||||
import net.corda.node.serialization.kryo.KryoCheckpointSerializer
|
||||
import net.corda.node.services.Permissions
|
||||
import net.corda.node.services.api.FlowStarter
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
@ -46,6 +44,7 @@ import net.corda.node.services.api.StartedNodeServices
|
||||
import net.corda.node.services.config.*
|
||||
import net.corda.node.services.messaging.*
|
||||
import net.corda.node.services.rpc.ArtemisRpcBroker
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.utilities.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.INTERNAL_SHELL_USER
|
||||
import net.corda.nodeapi.internal.ShutdownHook
|
||||
@ -59,7 +58,6 @@ import org.apache.commons.lang.SystemUtils
|
||||
import org.h2.jdbc.JdbcSQLException
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import rx.Observable
|
||||
import rx.Scheduler
|
||||
import rx.schedulers.Schedulers
|
||||
import java.net.BindException
|
||||
@ -75,8 +73,7 @@ import kotlin.system.exitProcess
|
||||
class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
val services: StartedNodeServices = object : StartedNodeServices, ServiceHubInternal by node.services, FlowStarter by node.flowStarter {}
|
||||
fun dispose() = node.stop()
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>): Observable<T> =
|
||||
node.registerInitiatedFlow(node.smm, initiatedFlowClass)
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(initiatedFlowClass: Class<T>) = node.registerInitiatedFlow(node.smm, initiatedFlowClass)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -88,14 +85,14 @@ class NodeWithInfo(val node: Node, val info: NodeInfo) {
|
||||
open class Node(configuration: NodeConfiguration,
|
||||
versionInfo: VersionInfo,
|
||||
private val initialiseSerialization: Boolean = true,
|
||||
cordappLoader: CordappLoader = makeCordappLoader(configuration, versionInfo),
|
||||
cacheFactoryPrototype: NamedCacheFactory = DefaultNamedCacheFactory()
|
||||
flowManager: FlowManager = NodeFlowManager(configuration.flowOverrides),
|
||||
cacheFactoryPrototype: BindableNamedCacheFactory = DefaultNamedCacheFactory()
|
||||
) : AbstractNode<NodeInfo>(
|
||||
configuration,
|
||||
createClock(configuration),
|
||||
cacheFactoryPrototype,
|
||||
versionInfo,
|
||||
cordappLoader,
|
||||
flowManager,
|
||||
// Under normal (non-test execution) it will always be "1"
|
||||
AffinityExecutor.ServiceAffinityExecutor("Node thread-${sameVmNodeCounter.incrementAndGet()}", 1)
|
||||
) {
|
||||
@ -114,9 +111,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
LoggerFactory.getLogger(loggerName).info(msg)
|
||||
}
|
||||
|
||||
fun printInRed(message: String) {
|
||||
println("${ShellConstants.RED}$message${ShellConstants.RESET}")
|
||||
}
|
||||
|
||||
fun printWarning(message: String) {
|
||||
Emoji.renderIfSupported {
|
||||
println("${Emoji.warningSign} ATTENTION: $message")
|
||||
printInRed("${Emoji.warningSign} ATTENTION: $message")
|
||||
}
|
||||
staticLog.warn(message)
|
||||
}
|
||||
@ -133,20 +134,16 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
private val sameVmNodeCounter = AtomicInteger()
|
||||
|
||||
private fun makeCordappLoader(configuration: NodeConfiguration, versionInfo: VersionInfo): CordappLoader {
|
||||
return JarScanningCordappLoader.fromDirectories(configuration.cordappDirectories, versionInfo)
|
||||
}
|
||||
|
||||
// TODO: make this configurable.
|
||||
const val MAX_RPC_MESSAGE_SIZE = 10485760
|
||||
|
||||
fun isValidJavaVersion(): Boolean {
|
||||
fun isInvalidJavaVersion(): Boolean {
|
||||
if (!hasMinimumJavaVersion()) {
|
||||
println("You are using a version of Java that is not supported (${SystemUtils.JAVA_VERSION}). Please upgrade to the latest version of Java 8.")
|
||||
println("Corda will now exit...")
|
||||
return false
|
||||
return true
|
||||
}
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
private fun hasMinimumJavaVersion(): Boolean {
|
||||
@ -211,7 +208,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
return P2PMessagingClient(
|
||||
config = configuration,
|
||||
versionInfo = versionInfo,
|
||||
serverAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
|
||||
serverAddress = configuration.messagingServerAddress
|
||||
?: NetworkHostAndPort("localhost", configuration.p2pAddress.port),
|
||||
nodeExecutor = serverThread,
|
||||
database = database,
|
||||
networkMap = networkMapCache,
|
||||
@ -232,12 +230,13 @@ open class Node(configuration: NodeConfiguration,
|
||||
val securityManagerConfig = configuration.security?.authService
|
||||
?: SecurityConfiguration.AuthService.fromUsers(configuration.rpcUsers)
|
||||
|
||||
val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig)) {
|
||||
val securityManager = with(RPCSecurityManagerImpl(securityManagerConfig, cacheFactory)) {
|
||||
if (configuration.shouldStartLocalShell()) RPCSecurityManagerWithAdditionalUser(this, User(INTERNAL_SHELL_USER, INTERNAL_SHELL_USER, setOf(Permissions.all()))) else this
|
||||
}
|
||||
|
||||
val messageBroker = if (!configuration.messagingServerExternal) {
|
||||
val brokerBindAddress = configuration.messagingServerAddress ?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
|
||||
val brokerBindAddress = configuration.messagingServerAddress
|
||||
?: NetworkHostAndPort("0.0.0.0", configuration.p2pAddress.port)
|
||||
ArtemisMessagingServer(configuration, brokerBindAddress, networkParameters.maxMessageSize)
|
||||
} else {
|
||||
null
|
||||
@ -276,7 +275,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
// Start up the MQ clients.
|
||||
internalRpcMessagingClient?.run {
|
||||
closeOnStop()
|
||||
init(rpcOps, securityManager)
|
||||
init(rpcOps, securityManager, cacheFactory)
|
||||
}
|
||||
network.closeOnStop()
|
||||
network.start(
|
||||
@ -451,7 +450,7 @@ open class Node(configuration: NodeConfiguration,
|
||||
}.build().start()
|
||||
}
|
||||
|
||||
private fun registerNewRelicReporter (registry: MetricRegistry) {
|
||||
private fun registerNewRelicReporter(registry: MetricRegistry) {
|
||||
log.info("Registering New Relic JMX Reporter:")
|
||||
val reporter = NewRelicReporter.forRegistry(registry)
|
||||
.name("New Relic Reporter")
|
||||
@ -470,17 +469,19 @@ open class Node(configuration: NodeConfiguration,
|
||||
private fun initialiseSerialization() {
|
||||
if (!initialiseSerialization) return
|
||||
val classloader = cordappLoader.appClassLoader
|
||||
nodeSerializationEnv = SerializationEnvironmentImpl(
|
||||
nodeSerializationEnv = SerializationEnvironment.with(
|
||||
SerializationFactoryImpl().apply {
|
||||
registerScheme(AMQPServerSerializationScheme(cordappLoader.cordapps))
|
||||
registerScheme(AMQPClientSerializationScheme(cordappLoader.cordapps))
|
||||
},
|
||||
checkpointSerializationFactory = CheckpointSerializationFactory(KryoSerializationScheme),
|
||||
p2pContext = AMQP_P2P_CONTEXT.withClassLoader(classloader),
|
||||
rpcServerContext = AMQP_RPC_SERVER_CONTEXT.withClassLoader(classloader),
|
||||
rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null, //even Shell embeded in the node connects via RPC to the node
|
||||
storageContext = AMQP_STORAGE_CONTEXT.withClassLoader(classloader),
|
||||
checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader),
|
||||
rpcClientContext = if (configuration.shouldInitCrashShell()) AMQP_RPC_CLIENT_CONTEXT.withClassLoader(classloader) else null) //even Shell embeded in the node connects via RPC to the node
|
||||
|
||||
checkpointSerializer = KryoCheckpointSerializer,
|
||||
checkpointContext = KRYO_CHECKPOINT_CONTEXT.withClassLoader(classloader)
|
||||
)
|
||||
}
|
||||
|
||||
/** Starts a blocking event loop for message dispatch. */
|
||||
@ -511,4 +512,8 @@ open class Node(configuration: NodeConfiguration,
|
||||
|
||||
log.info("Shutdown complete")
|
||||
}
|
||||
|
||||
fun <T : FlowLogic<*>> registerInitiatedFlow(smm: StateMachineManager, initiatedFlowClass: Class<T>) {
|
||||
this.flowManager.registerInitiatedFlow(initiatedFlowClass)
|
||||
}
|
||||
}
|
||||
|
@ -1,34 +1,24 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import io.netty.channel.unix.Errors
|
||||
import net.corda.cliutils.CordaCliWrapper
|
||||
import net.corda.cliutils.CordaVersionProvider
|
||||
import net.corda.cliutils.ExitCodes
|
||||
import net.corda.cliutils.*
|
||||
import net.corda.core.crypto.Crypto
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.concurrent.thenMatch
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.errors.AddressBindingException
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.*
|
||||
import net.corda.node.internal.Node.Companion.isValidJavaVersion
|
||||
import net.corda.node.internal.Node.Companion.isInvalidJavaVersion
|
||||
import net.corda.node.internal.cordapp.MultipleCordappsForFlowException
|
||||
import net.corda.node.internal.subcommands.*
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfigurationImpl
|
||||
import net.corda.node.services.config.shouldStartLocalShell
|
||||
import net.corda.node.services.config.shouldStartSSHDaemon
|
||||
import net.corda.node.services.transactions.bftSMaRtSerialFilter
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NodeRegistrationException
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import net.corda.nodeapi.internal.PLATFORM_VERSION
|
||||
import net.corda.nodeapi.internal.addShutdownHook
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigurationKeysException
|
||||
import net.corda.nodeapi.internal.persistence.CouldNotCreateDataSourceException
|
||||
@ -36,10 +26,8 @@ import net.corda.nodeapi.internal.persistence.DatabaseIncompatibleException
|
||||
import net.corda.tools.shell.InteractiveShell
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.slf4j.bridge.SLF4JBridgeHandler
|
||||
import picocli.CommandLine.Mixin
|
||||
import picocli.CommandLine.*
|
||||
import sun.misc.VMSupport
|
||||
import java.io.Console
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.RandomAccessFile
|
||||
import java.lang.management.ManagementFactory
|
||||
@ -48,215 +36,153 @@ import java.nio.file.Path
|
||||
import java.time.DayOfWeek
|
||||
import java.time.ZonedDateTime
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
/** This class is responsible for starting a Node from command line arguments. */
|
||||
open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
/** An interface that can be implemented to tell the node what to do once it's intitiated. */
|
||||
interface RunAfterNodeInitialisation {
|
||||
fun run(node: Node)
|
||||
}
|
||||
|
||||
/** Base class for subcommands to derive from that initialises the logs and provides standard options. */
|
||||
abstract class NodeCliCommand(alias: String, description: String, val startup: NodeStartup) : CliWrapperBase(alias, description), NodeStartupLogging {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
|
||||
const val LOGS_DIRECTORY_NAME = "logs"
|
||||
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
|
||||
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
|
||||
}
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = SharedNodeCmdLineOptions()
|
||||
}
|
||||
|
||||
/** Main corda entry point. */
|
||||
open class NodeStartupCli : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
val startup = NodeStartup()
|
||||
private val networkCacheCli = ClearNetworkCacheCli(startup)
|
||||
private val justGenerateNodeInfoCli = GenerateNodeInfoCli(startup)
|
||||
private val justGenerateRpcSslCertsCli = GenerateRpcSslCertsCli(startup)
|
||||
private val initialRegistrationCli = InitialRegistrationCli(startup)
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
override fun additionalSubCommands() = setOf(networkCacheCli, justGenerateNodeInfoCli, justGenerateRpcSslCertsCli, initialRegistrationCli)
|
||||
|
||||
override fun runProgram(): Int {
|
||||
return when {
|
||||
InitialRegistration.checkRegistrationMode(cmdLineOptions.baseDirectory) -> {
|
||||
println("Node was started before in `initial-registration` mode, but the registration was not completed.\nResuming registration.")
|
||||
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
initialRegistrationCli.runProgram()
|
||||
}
|
||||
//deal with legacy flags and redirect to subcommands
|
||||
cmdLineOptions.isRegistration -> {
|
||||
Node.printWarning("The --initial-registration flag has been deprecated and will be removed in a future version. Use the initial-registration command instead.")
|
||||
requireNotNull(cmdLineOptions.networkRootTrustStorePassword) { "Network root trust store password must be provided in registration mode using --network-root-truststore-password." }
|
||||
initialRegistrationCli.networkRootTrustStorePassword = cmdLineOptions.networkRootTrustStorePassword!!
|
||||
initialRegistrationCli.networkRootTrustStorePathParameter = cmdLineOptions.networkRootTrustStorePathParameter
|
||||
initialRegistrationCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
initialRegistrationCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.clearNetworkMapCache -> {
|
||||
Node.printWarning("The --clear-network-map-cache flag has been deprecated and will be removed in a future version. Use the clear-network-cache command instead.")
|
||||
networkCacheCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
networkCacheCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.justGenerateNodeInfo -> {
|
||||
Node.printWarning("The --just-generate-node-info flag has been deprecated and will be removed in a future version. Use the generate-node-info command instead.")
|
||||
justGenerateNodeInfoCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
justGenerateNodeInfoCli.runProgram()
|
||||
}
|
||||
cmdLineOptions.justGenerateRpcSslCerts -> {
|
||||
Node.printWarning("The --just-generate-rpc-ssl-settings flag has been deprecated and will be removed in a future version. Use the generate-rpc-ssl-settings command instead.")
|
||||
justGenerateRpcSslCertsCli.cmdLineOptions.copyFrom(cmdLineOptions)
|
||||
justGenerateRpcSslCertsCli.runProgram()
|
||||
}
|
||||
else -> startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
|
||||
val startupTime = System.currentTimeMillis()
|
||||
override fun run(node: Node) = startup.startNode(node, startupTime)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = NodeCmdLineOptions()
|
||||
}
|
||||
|
||||
/** This class provides a common set of functionality for starting a Node from command line arguments. */
|
||||
open class NodeStartup : NodeStartupLogging {
|
||||
companion object {
|
||||
private val logger by lazy { loggerFor<Node>() } // I guess this is lazy to allow for logging init, but why Node?
|
||||
const val LOGS_DIRECTORY_NAME = "logs"
|
||||
const val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in"
|
||||
}
|
||||
|
||||
lateinit var cmdLineOptions: SharedNodeCmdLineOptions
|
||||
|
||||
fun initialiseAndRun(cmdLineOptions: SharedNodeCmdLineOptions, afterNodeInitialisation: RunAfterNodeInitialisation): Int {
|
||||
this.cmdLineOptions = cmdLineOptions
|
||||
|
||||
/**
|
||||
* @return exit code based on the success of the node startup. This value is intended to be the exit code of the process.
|
||||
*/
|
||||
override fun runProgram(): Int {
|
||||
val startTime = System.currentTimeMillis()
|
||||
// Step 1. Check for supported Java version.
|
||||
if (!isValidJavaVersion()) return ExitCodes.FAILURE
|
||||
if (isInvalidJavaVersion()) return ExitCodes.FAILURE
|
||||
|
||||
// Step 2. We do the single node check before we initialise logging so that in case of a double-node start it
|
||||
// doesn't mess with the running node's logs.
|
||||
enforceSingleNodeIsRunning(cmdLineOptions.baseDirectory)
|
||||
|
||||
// Step 3. Initialise logging.
|
||||
initLogging()
|
||||
|
||||
// Step 4. Register all cryptography [Provider]s.
|
||||
// Step 3. Register all cryptography [Provider]s.
|
||||
// Required to install our [SecureRandom] before e.g., UUID asks for one.
|
||||
// This needs to go after initLogging(netty clashes with our logging).
|
||||
// This needs to go after initLogging(netty clashes with our logging)
|
||||
Crypto.registerProviders()
|
||||
|
||||
// Step 5. Print banner and basic node info.
|
||||
// Step 4. Print banner and basic node info.
|
||||
val versionInfo = getVersionInfo()
|
||||
drawBanner(versionInfo)
|
||||
Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path"))
|
||||
|
||||
// Step 6. Load and validate node configuration.
|
||||
val configuration = (attempt { loadConfiguration() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value) ?: return ExitCodes.FAILURE
|
||||
// Step 5. Load and validate node configuration.
|
||||
val configuration = (attempt { cmdLineOptions.loadConfig() }.doOnException(handleConfigurationLoadingError(cmdLineOptions.configFile)) as? Try.Success)?.let(Try.Success<NodeConfiguration>::value)
|
||||
?: return ExitCodes.FAILURE
|
||||
val errors = configuration.validate()
|
||||
if (errors.isNotEmpty()) {
|
||||
logger.error("Invalid node configuration. Errors were:${System.lineSeparator()}${errors.joinToString(System.lineSeparator())}")
|
||||
return ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
// Step 7. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 6. Configuring special serialisation requirements, i.e., bft-smart relies on Java serialization.
|
||||
attempt { banJavaSerialisation(configuration) }.doOnException { error -> error.logAsUnexpected("Exception while configuring serialisation") } as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
|
||||
// Step 8. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(handleRegistrationError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 7. Any actions required before starting up the Corda network layer.
|
||||
attempt { preNetworkRegistration(configuration) }.doOnException(::handleRegistrationError) as? Try.Success
|
||||
?: return ExitCodes.FAILURE
|
||||
|
||||
// Step 9. Check if in registration mode.
|
||||
checkAndRunRegistrationMode(configuration, versionInfo)?.let {
|
||||
return if (it) ExitCodes.SUCCESS
|
||||
else ExitCodes.FAILURE
|
||||
}
|
||||
|
||||
// Step 10. Log startup info.
|
||||
// Step 8. Log startup info.
|
||||
logStartupInfo(versionInfo, configuration)
|
||||
|
||||
// Step 11. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt { startNode(configuration, versionInfo, startTime) }.doOnSuccess { logger.info("Node exiting successfully") }.doOnException(handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
// Step 9. Start node: create the node, check for other command-line options, add extra logging etc.
|
||||
attempt {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
afterNodeInitialisation.run(createNode(configuration, versionInfo))
|
||||
}.doOnException(::handleStartError) as? Try.Success ?: return ExitCodes.FAILURE
|
||||
|
||||
return ExitCodes.SUCCESS
|
||||
}
|
||||
|
||||
private fun checkAndRunRegistrationMode(configuration: NodeConfiguration, versionInfo: VersionInfo): Boolean? {
|
||||
checkUnfinishedRegistration()
|
||||
cmdLineOptions.nodeRegistrationOption?.let {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(configuration, versionInfo, it) }.doOnException(handleRegistrationError) as? Try.Success
|
||||
?: return false
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(cmdLineOptions.baseDirectory)
|
||||
return true
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Reconsider if automatic re-registration should be applied when something failed during initial registration.
|
||||
// There might be cases where the node user should investigate what went wrong before registering again.
|
||||
private fun checkUnfinishedRegistration() {
|
||||
if (checkRegistrationMode() && !cmdLineOptions.isRegistration) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
// Pretend that the node was started with `--initial-registration` to help prevent user error.
|
||||
cmdLineOptions.isRegistration = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
|
||||
private fun Exception.isExpectedWhenStartingNode() = startNodeExpectedErrors.any { error -> error.isInstance(this) }
|
||||
|
||||
private val startNodeExpectedErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
|
||||
private fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
|
||||
|
||||
private fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
|
||||
|
||||
private fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
private val handleRegistrationError = { error: Exception ->
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Node registration service is unavailable. Perhaps try to perform the initial registration again after a while.")
|
||||
else -> error.logAsUnexpected("Exception during node registration")
|
||||
}
|
||||
}
|
||||
|
||||
private val handleStartError = { error: Exception ->
|
||||
when {
|
||||
error.isExpectedWhenStartingNode() -> error.logAsExpected()
|
||||
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
|
||||
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
|
||||
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
else -> error.logAsUnexpected("Exception during node startup")
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
|
||||
when (error) {
|
||||
is UnknownConfigurationKeysException -> error.logAsExpected()
|
||||
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
|
||||
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun configFileNotFoundMessage(configFile: Path): String {
|
||||
return """
|
||||
Unable to load the node config file from '$configFile'.
|
||||
|
||||
Try setting the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
private fun loadConfiguration(): NodeConfiguration {
|
||||
val (rawConfig, configurationResult) = loadConfigFile()
|
||||
if (cmdLineOptions.devMode == true) {
|
||||
println("Config:\n${rawConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
}
|
||||
val configuration = configurationResult.getOrThrow()
|
||||
return if (cmdLineOptions.bootstrapRaftCluster) {
|
||||
println("Bootstrapping raft cluster (starting up as seed node).")
|
||||
// Ignore the configured clusterAddresses to make the node bootstrap a cluster instead of joining.
|
||||
(configuration as NodeConfigurationImpl).copy(notary = configuration.notary?.copy(raft = configuration.notary?.raft?.copy(clusterAddresses = emptyList())))
|
||||
} else {
|
||||
configuration
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkRegistrationMode(): Boolean {
|
||||
// If the node was started with `--initial-registration`, create marker file.
|
||||
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
|
||||
val marker = cmdLineOptions.baseDirectory / INITIAL_REGISTRATION_MARKER
|
||||
if (!cmdLineOptions.isRegistration && !marker.exists()) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
marker.createFile()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not create marker file for `--initial-registration`.", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun deleteNodeRegistrationMarker(baseDir: Path) {
|
||||
try {
|
||||
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
|
||||
if (marker.exists()) {
|
||||
marker.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.logAsUnexpected("Could not delete the marker file that was created for `--initial-registration`.", print = logger::warn)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun preNetworkRegistration(conf: NodeConfiguration) = Unit
|
||||
|
||||
protected open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
open fun createNode(conf: NodeConfiguration, versionInfo: VersionInfo): Node = Node(conf, versionInfo)
|
||||
|
||||
protected open fun startNode(conf: NodeConfiguration, versionInfo: VersionInfo, startTime: Long) {
|
||||
cmdLineOptions.baseDirectory.createDirectories()
|
||||
val node = createNode(conf, versionInfo)
|
||||
if (cmdLineOptions.clearNetworkMapCache) {
|
||||
node.clearNetworkMapCache()
|
||||
return
|
||||
}
|
||||
if (cmdLineOptions.justGenerateNodeInfo) {
|
||||
// Perform the minimum required start-up logic to be able to write a nodeInfo to disk
|
||||
node.generateAndSaveNodeInfo()
|
||||
return
|
||||
}
|
||||
if (cmdLineOptions.justGenerateRpcSslCerts) {
|
||||
generateRpcSslCertificates(conf)
|
||||
return
|
||||
}
|
||||
|
||||
if (conf.devMode) {
|
||||
fun startNode(node: Node, startTime: Long) {
|
||||
if (node.configuration.devMode) {
|
||||
Emoji.renderIfSupported {
|
||||
Node.printWarning("This node is running in developer mode! ${Emoji.developer} This is not safe for production deployment.")
|
||||
Node.printWarning("This node is running in development mode! ${Emoji.developer} This is not safe for production deployment.")
|
||||
}
|
||||
} else {
|
||||
logger.info("The Corda node is running in production mode. If this is a developer environment you can set 'devMode=true' in the node.conf file.")
|
||||
}
|
||||
|
||||
val nodeInfo = node.start()
|
||||
logLoadedCorDapps(node.services.cordappProvider.cordapps)
|
||||
val loadedCodapps = node.services.cordappProvider.cordapps.filter { it.isLoaded }
|
||||
logLoadedCorDapps(loadedCodapps)
|
||||
|
||||
node.nodeReadyFuture.thenMatch({
|
||||
// Elapsed time in seconds. We used 10 / 100.0 and not directly / 1000.0 to only keep two decimal digits.
|
||||
@ -265,7 +191,7 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
Node.printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec")
|
||||
|
||||
// Don't start the shell if there's no console attached.
|
||||
if (conf.shouldStartLocalShell()) {
|
||||
if (node.configuration.shouldStartLocalShell()) {
|
||||
node.startupComplete.then {
|
||||
try {
|
||||
InteractiveShell.runLocalShell(node::stop)
|
||||
@ -274,8 +200,8 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (conf.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", conf.sshd!!.port.toString())
|
||||
if (node.configuration.shouldStartSSHDaemon()) {
|
||||
Node.printBasicNodeInfo("SSH server listening on port", node.configuration.sshd!!.port.toString())
|
||||
}
|
||||
},
|
||||
{ th ->
|
||||
@ -284,82 +210,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
node.run()
|
||||
}
|
||||
|
||||
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting..")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit.
|
||||
null -> {
|
||||
println("Not connected to console. Exiting")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally.
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the RPC keystore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (keystorePassword1.isEmpty()) {
|
||||
println("The RPC keystore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password => ")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The RPC keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The RPC keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password => ")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (trustStorePassword1.isEmpty()) {
|
||||
println("The RPC truststore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password => ")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The RPC truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The RPC truststore was saved to: $trustStorePath .")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates for RPC were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun logStartupInfo(versionInfo: VersionInfo, conf: NodeConfiguration) {
|
||||
logger.info("Vendor: ${versionInfo.vendor}")
|
||||
logger.info("Release: ${versionInfo.releaseVersion}")
|
||||
@ -378,36 +228,19 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
if (agentProperties.containsKey("sun.jdwp.listenerAddress")) {
|
||||
logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}")
|
||||
}
|
||||
logger.info("Starting as node on ${conf.p2pAddress}")
|
||||
var nodeStartedMessage = "Starting as node on ${conf.p2pAddress}"
|
||||
if (conf.extraNetworkMapKeys.isNotEmpty()) {
|
||||
nodeStartedMessage = "$nodeStartedMessage with additional Network Map keys ${conf.extraNetworkMapKeys.joinToString(prefix = "[", postfix = "]", separator = ", ")}"
|
||||
}
|
||||
logger.info(nodeStartedMessage)
|
||||
}
|
||||
|
||||
protected open fun registerWithNetwork(conf: NodeConfiguration, versionInfo: VersionInfo, nodeRegistrationConfig: NodeRegistrationOption) {
|
||||
val compatibilityZoneURL = conf.networkServices?.doormanURL ?: throw RuntimeException(
|
||||
"compatibilityZoneURL or networkServices must be configured!")
|
||||
|
||||
println()
|
||||
println("******************************************************************")
|
||||
println("* *")
|
||||
println("* Registering as a new participant with Corda network *")
|
||||
println("* *")
|
||||
println("******************************************************************")
|
||||
NodeRegistrationHelper(conf, HTTPNetworkRegistrationService(compatibilityZoneURL, versionInfo), nodeRegistrationConfig).buildKeystore()
|
||||
|
||||
// Minimal changes to make registration tool create node identity.
|
||||
// TODO: Move node identity generation logic from node to registration helper.
|
||||
createNode(conf, getVersionInfo()).generateAndSaveNodeInfo()
|
||||
|
||||
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
|
||||
println("Corda node will now terminate.")
|
||||
}
|
||||
|
||||
protected open fun loadConfigFile(): Pair<Config, Try<NodeConfiguration>> = cmdLineOptions.loadConfig()
|
||||
|
||||
protected open fun banJavaSerialisation(conf: NodeConfiguration) {
|
||||
SerialFilter.install(if (conf.notary?.bftSMaRt != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter)
|
||||
// Note that in dev mode this filter can be overridden by a notary service implementation.
|
||||
SerialFilter.install(::defaultSerialFilter)
|
||||
}
|
||||
|
||||
protected open fun getVersionInfo(): VersionInfo {
|
||||
open fun getVersionInfo(): VersionInfo {
|
||||
return VersionInfo(
|
||||
PLATFORM_VERSION,
|
||||
CordaVersionProvider.releaseVersion,
|
||||
@ -460,18 +293,6 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
|
||||
override fun initLogging() {
|
||||
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
Node.renderBasicInfoToConsole = false
|
||||
}
|
||||
System.setProperty("log-path", (cmdLineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
|
||||
private fun lookupMachineNameAndMaybeWarn(): String {
|
||||
val start = System.currentTimeMillis()
|
||||
val hostName: String = InetAddress.getLocalHost().hostName
|
||||
@ -575,3 +396,66 @@ open class NodeStartup : CordaCliWrapper("corda", "Runs a Corda Node") {
|
||||
}
|
||||
}
|
||||
|
||||
/** Provide some common logging methods for node startup commands. */
|
||||
interface NodeStartupLogging {
|
||||
companion object {
|
||||
val logger by lazy { contextLogger() }
|
||||
val startupErrors = setOf(MultipleCordappsForFlowException::class, CheckpointIncompatibleException::class, AddressBindingException::class, NetworkParametersReader::class, DatabaseIncompatibleException::class)
|
||||
}
|
||||
|
||||
fun <RESULT> attempt(action: () -> RESULT): Try<RESULT> = Try.on(action)
|
||||
|
||||
fun Exception.logAsExpected(message: String? = this.message, print: (String?) -> Unit = logger::error) = print(message)
|
||||
|
||||
fun Exception.logAsUnexpected(message: String? = this.message, error: Exception = this, print: (String?, Throwable) -> Unit = logger::error) = print("$message${this.message?.let { ": $it" } ?: ""}", error)
|
||||
|
||||
fun handleRegistrationError(error: Exception) {
|
||||
when (error) {
|
||||
is NodeRegistrationException -> error.logAsExpected("Issue with Node registration: ${error.message}")
|
||||
else -> error.logAsUnexpected("Exception during node registration")
|
||||
}
|
||||
}
|
||||
|
||||
fun Exception.isOpenJdkKnownIssue() = message?.startsWith("Unknown named curve:") == true
|
||||
|
||||
fun Exception.isExpectedWhenStartingNode() = startupErrors.any { error -> error.isInstance(this) }
|
||||
|
||||
fun handleStartError(error: Exception) {
|
||||
when {
|
||||
error.isExpectedWhenStartingNode() -> error.logAsExpected()
|
||||
error is CouldNotCreateDataSourceException -> error.logAsUnexpected()
|
||||
error is Errors.NativeIoException && error.message?.contains("Address already in use") == true -> error.logAsExpected("One of the ports required by the Corda node is already in use.")
|
||||
error.isOpenJdkKnownIssue() -> error.logAsExpected("Exception during node startup - ${error.message}. This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.")
|
||||
else -> error.logAsUnexpected("Exception during node startup")
|
||||
}
|
||||
}
|
||||
|
||||
fun handleConfigurationLoadingError(configFile: Path) = { error: Exception ->
|
||||
when (error) {
|
||||
is UnknownConfigurationKeysException -> error.logAsExpected()
|
||||
is ConfigException.IO -> error.logAsExpected(configFileNotFoundMessage(configFile), ::println)
|
||||
else -> error.logAsUnexpected("Unexpected error whilst reading node configuration")
|
||||
}
|
||||
}
|
||||
|
||||
private fun configFileNotFoundMessage(configFile: Path): String {
|
||||
return """
|
||||
Unable to load the node config file from '$configFile'.
|
||||
|
||||
Try setting the --base-directory flag to change which directory the node
|
||||
is looking in, or use the --config-file flag to specify it explicitly.
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
||||
fun CliWrapperBase.initLogging(baseDirectory: Path) {
|
||||
val loggingLevel = loggingLevel.name.toLowerCase(Locale.ENGLISH)
|
||||
System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file.
|
||||
if (verbose) {
|
||||
System.setProperty("consoleLogLevel", loggingLevel)
|
||||
Node.renderBasicInfoToConsole = false
|
||||
}
|
||||
System.setProperty("log-path", (baseDirectory / NodeCliCommand.LOGS_DIRECTORY_NAME).toString())
|
||||
SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler.
|
||||
SLF4JBridgeHandler.install()
|
||||
}
|
||||
|
@ -5,30 +5,26 @@ import com.typesafe.config.ConfigFactory
|
||||
import net.corda.core.internal.createDirectories
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.internal.isDirectory
|
||||
import net.corda.core.internal.noneOrSingle
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class CordappConfigFileProvider(private val configDir: Path = DEFAULT_CORDAPP_CONFIG_DIR) : CordappConfigProvider {
|
||||
class CordappConfigFileProvider(cordappDirectories: List<Path>) : CordappConfigProvider {
|
||||
companion object {
|
||||
val DEFAULT_CORDAPP_CONFIG_DIR = Paths.get("cordapps") / "config"
|
||||
const val CONFIG_EXT = ".conf"
|
||||
val logger = contextLogger()
|
||||
private val logger = contextLogger()
|
||||
}
|
||||
|
||||
init {
|
||||
configDir.createDirectories()
|
||||
}
|
||||
private val configDirectories = cordappDirectories.map { (it / "config").createDirectories() }
|
||||
|
||||
override fun getConfigByName(name: String): Config {
|
||||
val configFile = configDir / "$name$CONFIG_EXT"
|
||||
return if (configFile.exists()) {
|
||||
check(!configFile.isDirectory()) { "${configFile.toAbsolutePath()} is a directory, expected a config file" }
|
||||
logger.info("Found config for cordapp $name in ${configFile.toAbsolutePath()}")
|
||||
// TODO There's nothing stopping the same CorDapp jar from occuring in different directories and thus causing
|
||||
// conflicts. The cordappDirectories list config option should just be a single cordappDirectory
|
||||
val configFile = configDirectories.map { it / "$name.conf" }.noneOrSingle { it.exists() }
|
||||
return if (configFile != null) {
|
||||
logger.info("Found config for cordapp $name in $configFile")
|
||||
ConfigFactory.parseFile(configFile.toFile())
|
||||
} else {
|
||||
logger.info("No config found for cordapp $name in ${configFile.toAbsolutePath()}")
|
||||
logger.info("No config found for cordapp $name in $configDirectories")
|
||||
ConfigFactory.empty()
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner
|
||||
import io.github.lukehutch.fastclasspathscanner.scanner.ScanResult
|
||||
import net.corda.core.contracts.warnContractWithoutConstraintPropagation
|
||||
import io.github.classgraph.ClassGraph
|
||||
import io.github.classgraph.ScanResult
|
||||
import net.corda.core.cordapp.Cordapp
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
@ -10,6 +9,8 @@ import net.corda.core.flows.*
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.cordapp.CordappInfoResolver
|
||||
import net.corda.core.internal.notary.NotaryService
|
||||
import net.corda.core.internal.notary.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.services.CordaService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.serialization.SerializationCustomSerializer
|
||||
@ -18,7 +19,6 @@ import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.cordapp.CordappLoader
|
||||
import net.corda.node.internal.classloading.requireAnnotation
|
||||
import net.corda.nodeapi.internal.coreContractClasses
|
||||
import net.corda.serialization.internal.DefaultWhitelist
|
||||
import org.apache.commons.collections4.map.LRUMap
|
||||
@ -26,6 +26,7 @@ import java.lang.reflect.Modifier
|
||||
import java.net.URL
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Path
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
import java.util.jar.JarInputStream
|
||||
import kotlin.reflect.KClass
|
||||
@ -37,9 +38,13 @@ import kotlin.streams.toList
|
||||
* @property cordappJarPaths The classpath of cordapp JARs
|
||||
*/
|
||||
class JarScanningCordappLoader private constructor(private val cordappJarPaths: List<RestrictedURL>,
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN) : CordappLoaderTemplate() {
|
||||
private val versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl>,
|
||||
private val blacklistedCordappSigners: List<X509Certificate> = emptyList()) : CordappLoaderTemplate() {
|
||||
|
||||
override val cordapps: List<CordappImpl> by lazy { loadCordapps() + coreCordapp }
|
||||
override val cordapps: List<CordappImpl> by lazy {
|
||||
loadCordapps() + extraCordapps
|
||||
}
|
||||
|
||||
override val appClassLoader: ClassLoader = URLClassLoader(cordappJarPaths.stream().map { it.url }.toTypedArray(), javaClass.classLoader)
|
||||
|
||||
@ -57,11 +62,15 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
/**
|
||||
* Creates a CordappLoader from multiple directories.
|
||||
*
|
||||
* @param corDappDirectories Directories used to scan for CorDapp JARs.
|
||||
* @param cordappDirs Directories used to scan for CorDapp JARs.
|
||||
*/
|
||||
fun fromDirectories(corDappDirectories: Iterable<Path>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader {
|
||||
logger.info("Looking for CorDapps in ${corDappDirectories.distinct().joinToString(", ", "[", "]")}")
|
||||
return JarScanningCordappLoader(corDappDirectories.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }, versionInfo)
|
||||
fun fromDirectories(cordappDirs: Collection<Path>,
|
||||
versionInfo: VersionInfo = VersionInfo.UNKNOWN,
|
||||
extraCordapps: List<CordappImpl> = emptyList(),
|
||||
blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
|
||||
logger.info("Looking for CorDapps in ${cordappDirs.distinct().joinToString(", ", "[", "]")}")
|
||||
val paths = cordappDirs.distinct().flatMap(this::jarUrlsInDirectory).map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,8 +78,9 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
*
|
||||
* @param scanJars Uses the JAR URLs provided for classpath scanning and Cordapp detection.
|
||||
*/
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN): JarScanningCordappLoader {
|
||||
return JarScanningCordappLoader(scanJars.map { it.restricted() }, versionInfo)
|
||||
fun fromJarUrls(scanJars: List<URL>, versionInfo: VersionInfo = VersionInfo.UNKNOWN, extraCordapps: List<CordappImpl> = emptyList(), blacklistedCerts: List<X509Certificate> = emptyList()): JarScanningCordappLoader {
|
||||
val paths = scanJars.map { it.restricted() }
|
||||
return JarScanningCordappLoader(paths, versionInfo, extraCordapps, blacklistedCerts)
|
||||
}
|
||||
|
||||
private fun URL.restricted(rootPackageName: String? = null) = RestrictedURL(this, rootPackageName)
|
||||
@ -86,48 +96,38 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A list of the core RPC flows present in Corda */
|
||||
private val coreRPCFlows = listOf(
|
||||
ContractUpgradeFlow.Initiate::class.java,
|
||||
ContractUpgradeFlow.Authorise::class.java,
|
||||
ContractUpgradeFlow.Deauthorise::class.java)
|
||||
}
|
||||
|
||||
/** A Cordapp representing the core package which is not scanned automatically. */
|
||||
@VisibleForTesting
|
||||
internal val coreCordapp = CordappImpl(
|
||||
contractClassNames = listOf(),
|
||||
initiatedFlows = listOf(),
|
||||
rpcFlows = coreRPCFlows,
|
||||
serviceFlows = listOf(),
|
||||
schedulableFlows = listOf(),
|
||||
services = listOf(),
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(),
|
||||
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
|
||||
allFlows = listOf(),
|
||||
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
|
||||
jarHash = SecureHash.allOnesHash
|
||||
)
|
||||
|
||||
private fun loadCordapps(): List<CordappImpl> {
|
||||
val cordapps = cordappJarPaths.map { scanCordapp(it).toCordapp(it) }
|
||||
val cordapps = cordappJarPaths
|
||||
.map { url -> scanCordapp(url).use { it.toCordapp(url) } }
|
||||
.filter {
|
||||
if (it.info.minimumPlatformVersion > versionInfo.platformVersion) {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it requires minimum " +
|
||||
"platform version ${it.info.minimumPlatformVersion} (This node is running version ${versionInfo.platformVersion}).")
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
.filter {
|
||||
if (blacklistedCordappSigners.isEmpty()) {
|
||||
true //Nothing blacklisted, no need to check
|
||||
} else {
|
||||
val certificates = it.jarPath.openStream().let(::JarInputStream).use(JarSignatureCollector::collectCertificates)
|
||||
if (certificates.isEmpty() || (certificates - blacklistedCordappSigners).isNotEmpty())
|
||||
true // Cordapp is not signed or it is signed by at least one non-blacklisted certificate
|
||||
else {
|
||||
logger.warn("Not loading CorDapp ${it.info.shortName} (${it.info.vendor}) as it is signed by development key(s) only: " +
|
||||
"${certificates.intersect(blacklistedCordappSigners).map { it.publicKey }}.")
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
cordapps.forEach { CordappInfoResolver.register(it.cordappClasses, it.info) }
|
||||
return cordapps
|
||||
}
|
||||
|
||||
private fun RestrictedScanResult.toCordapp(url: RestrictedURL): CordappImpl {
|
||||
val info = url.url.openStream().let(::JarInputStream).use { it.manifest }.toCordappInfo(CordappImpl.jarName(url.url))
|
||||
val info = url.url.openStream().let(::JarInputStream).use { it.manifest?.toCordappInfo(CordappImpl.jarName(url.url)) ?: CordappImpl.Info.UNKNOWN }
|
||||
return CordappImpl(
|
||||
findContractClassNames(this),
|
||||
findInitiatedFlows(this),
|
||||
@ -141,10 +141,21 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
findAllFlows(this),
|
||||
url.url,
|
||||
info,
|
||||
getJarHash(url.url)
|
||||
getJarHash(url.url),
|
||||
findNotaryService(this)
|
||||
)
|
||||
}
|
||||
|
||||
private fun findNotaryService(scanResult: RestrictedScanResult): Class<out NotaryService>? {
|
||||
// Note: we search for implementations of both NotaryService and TrustedAuthorityNotaryService as
|
||||
// the scanner won't find subclasses deeper down the hierarchy if any intermediate class is not
|
||||
// present in the CorDapp.
|
||||
val result = scanResult.getClassesWithSuperclass(NotaryService::class) +
|
||||
scanResult.getClassesWithSuperclass(TrustedAuthorityNotaryService::class)
|
||||
logger.info("Found notary service CorDapp implementations: " + result.joinToString(", "))
|
||||
return result.firstOrNull()
|
||||
}
|
||||
|
||||
private fun getJarHash(url: URL): SecureHash.SHA256 = url.openStream().readFully().sha256()
|
||||
|
||||
private fun findServices(scanResult: RestrictedScanResult): List<Class<out SerializeAsToken>> {
|
||||
@ -153,17 +164,6 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
|
||||
private fun findInitiatedFlows(scanResult: RestrictedScanResult): List<Class<out FlowLogic<*>>> {
|
||||
return scanResult.getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class)
|
||||
// First group by the initiating flow class in case there are multiple mappings
|
||||
.groupBy { it.requireAnnotation<InitiatedBy>().value.java }
|
||||
.map { (initiatingFlow, initiatedFlows) ->
|
||||
val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow))
|
||||
if (sorted.size > 1) {
|
||||
logger.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " +
|
||||
"in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " +
|
||||
"specific sub-type for registration: ${sorted[0].name}.")
|
||||
}
|
||||
sorted[0]
|
||||
}
|
||||
}
|
||||
|
||||
private fun Class<out FlowLogic<*>>.isUserInvokable(): Boolean {
|
||||
@ -205,7 +205,7 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
}
|
||||
|
||||
private fun findCustomSchemas(scanResult: RestrictedScanResult): Set<MappedSchema> {
|
||||
return scanResult.getClassesWithSuperclass(MappedSchema::class).toSet()
|
||||
return scanResult.getClassesWithSuperclass(MappedSchema::class).instances().toSet()
|
||||
}
|
||||
|
||||
private val cachedScanResult = LRUMap<RestrictedURL, RestrictedScanResult>(1000)
|
||||
@ -213,21 +213,12 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
private fun scanCordapp(cordappJarPath: RestrictedURL): RestrictedScanResult {
|
||||
logger.info("Scanning CorDapp in ${cordappJarPath.url}")
|
||||
return cachedScanResult.computeIfAbsent(cordappJarPath) {
|
||||
RestrictedScanResult(FastClasspathScanner().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).scan(), cordappJarPath.qualifiedNamePrefix)
|
||||
val scanResult = ClassGraph().addClassLoader(appClassLoader).overrideClasspath(cordappJarPath.url).enableAllInfo().scan()
|
||||
RestrictedScanResult(scanResult, cordappJarPath.qualifiedNamePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
private class FlowTypeHierarchyComparator(val initiatingFlow: Class<out FlowLogic<*>>) : Comparator<Class<out FlowLogic<*>>> {
|
||||
override fun compare(o1: Class<out FlowLogic<*>>, o2: Class<out FlowLogic<*>>): Int {
|
||||
return when {
|
||||
o1 == o2 -> 0
|
||||
o1.isAssignableFrom(o2) -> 1
|
||||
o2.isAssignableFrom(o1) -> -1
|
||||
else -> throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " +
|
||||
"both ${o1.name} and ${o2.name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun <T : Any> loadClass(className: String, type: KClass<T>): Class<out T>? {
|
||||
return try {
|
||||
@ -246,41 +237,53 @@ class JarScanningCordappLoader private constructor(private val cordappJarPaths:
|
||||
val qualifiedNamePrefix: String get() = rootPackageName?.let { "$it." } ?: ""
|
||||
}
|
||||
|
||||
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) {
|
||||
private fun <T : Any> List<Class<out T>>.instances(): List<T> {
|
||||
return map { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
|
||||
private inner class RestrictedScanResult(private val scanResult: ScanResult, private val qualifiedNamePrefix: String) : AutoCloseable {
|
||||
fun getNamesOfClassesImplementing(type: KClass<*>): List<String> {
|
||||
return scanResult.getNamesOfClassesImplementing(type.java)
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
return scanResult.getClassesImplementing(type.java.name).names.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
}
|
||||
|
||||
fun <T : Any> getClassesWithSuperclass(type: KClass<T>): List<T> {
|
||||
return scanResult.getNamesOfSubclassesOf(type.java)
|
||||
fun <T : Any> getClassesWithSuperclass(type: KClass<T>): List<Class<out T>> {
|
||||
return scanResult
|
||||
.getSubclasses(type.java.name)
|
||||
.names
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull { loadClass(it, type) }
|
||||
.filterNot { Modifier.isAbstract(it.modifiers) }
|
||||
.map { it.kotlin.objectOrNewInstance() }
|
||||
.filterNot { it.isAbstractClass }
|
||||
}
|
||||
|
||||
fun <T : Any> getClassesImplementing(type: KClass<T>): List<T> {
|
||||
return scanResult.getNamesOfClassesImplementing(type.java)
|
||||
return scanResult
|
||||
.getClassesImplementing(type.java.name)
|
||||
.names
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull { loadClass(it, type) }
|
||||
.filterNot { Modifier.isAbstract(it.modifiers) }
|
||||
.filterNot { it.isAbstractClass }
|
||||
.map { it.kotlin.objectOrNewInstance() }
|
||||
}
|
||||
|
||||
fun <T : Any> getClassesWithAnnotation(type: KClass<T>, annotation: KClass<out Annotation>): List<Class<out T>> {
|
||||
return scanResult.getNamesOfClassesWithAnnotation(annotation.java)
|
||||
return scanResult
|
||||
.getClassesWithAnnotation(annotation.java.name)
|
||||
.names
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull { loadClass(it, type) }
|
||||
.filterNot { Modifier.isAbstract(it.modifiers) }
|
||||
}
|
||||
|
||||
fun <T : Any> getConcreteClassesOfType(type: KClass<T>): List<Class<out T>> {
|
||||
return scanResult.getNamesOfSubclassesOf(type.java)
|
||||
return scanResult
|
||||
.getSubclasses(type.java.name)
|
||||
.names
|
||||
.filter { it.startsWith(qualifiedNamePrefix) }
|
||||
.mapNotNull { loadClass(it, type) }
|
||||
.filterNot { Modifier.isAbstract(it.modifiers) }
|
||||
.filterNot { it.isAbstractClass }
|
||||
}
|
||||
|
||||
override fun close() = scanResult.close()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.cordapp.CordappImpl.Info.Companion.UNKNOWN_VALUE
|
||||
import java.util.jar.Attributes
|
||||
import java.util.jar.Manifest
|
||||
|
||||
fun createTestManifest(name: String, title: String, version: String, vendor: String): Manifest {
|
||||
fun createTestManifest(name: String, title: String, version: String, vendor: String, targetVersion: Int): Manifest {
|
||||
val manifest = Manifest()
|
||||
|
||||
// Mandatory manifest attribute. If not present, all other entries are silently skipped.
|
||||
@ -19,27 +20,28 @@ fun createTestManifest(name: String, title: String, version: String, vendor: Str
|
||||
manifest["Implementation-Title"] = title
|
||||
manifest["Implementation-Version"] = version
|
||||
manifest["Implementation-Vendor"] = vendor
|
||||
manifest["Target-Platform-Version"] = targetVersion.toString()
|
||||
|
||||
return manifest
|
||||
}
|
||||
|
||||
operator fun Manifest.set(key: String, value: String) {
|
||||
mainAttributes.putValue(key, value)
|
||||
operator fun Manifest.set(key: String, value: String): String? {
|
||||
return mainAttributes.putValue(key, value)
|
||||
}
|
||||
|
||||
fun Manifest?.toCordappInfo(defaultShortName: String): CordappImpl.Info {
|
||||
var info = CordappImpl.Info.UNKNOWN
|
||||
(this?.mainAttributes?.getValue("Name") ?: defaultShortName).let { shortName ->
|
||||
info = info.copy(shortName = shortName)
|
||||
}
|
||||
this?.mainAttributes?.getValue("Implementation-Vendor")?.let { vendor ->
|
||||
info = info.copy(vendor = vendor)
|
||||
}
|
||||
this?.mainAttributes?.getValue("Implementation-Version")?.let { version ->
|
||||
info = info.copy(version = version)
|
||||
}
|
||||
val minPlatformVersion = this?.mainAttributes?.getValue("Min-Platform-Version")?.toInt() ?: 1
|
||||
val targetPlatformVersion = this?.mainAttributes?.getValue("Target-Platform-Version")?.toInt() ?: minPlatformVersion
|
||||
info = info.copy(minimumPlatformVersion = minPlatformVersion, targetPlatformVersion = targetPlatformVersion)
|
||||
return info
|
||||
}
|
||||
operator fun Manifest.get(key: String): String? = mainAttributes.getValue(key)
|
||||
|
||||
fun Manifest.toCordappInfo(defaultShortName: String): CordappImpl.Info {
|
||||
val shortName = this["Name"] ?: defaultShortName
|
||||
val vendor = this["Implementation-Vendor"] ?: UNKNOWN_VALUE
|
||||
val version = this["Implementation-Version"] ?: UNKNOWN_VALUE
|
||||
val minPlatformVersion = this["Min-Platform-Version"]?.toIntOrNull() ?: 1
|
||||
val targetPlatformVersion = this["Target-Platform-Version"]?.toIntOrNull() ?: minPlatformVersion
|
||||
return CordappImpl.Info(
|
||||
shortName = shortName,
|
||||
vendor = vendor,
|
||||
version = version,
|
||||
minimumPlatformVersion = minPlatformVersion,
|
||||
targetPlatformVersion = targetPlatformVersion
|
||||
)
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
package net.corda.node.internal.cordapp
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.ContractUpgradeFlow
|
||||
import net.corda.core.internal.cordapp.CordappImpl
|
||||
import net.corda.core.internal.location
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.services.transactions.NodeNotarySchemaV1
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
|
||||
internal object VirtualCordapp {
|
||||
/** A list of the core RPC flows present in Corda */
|
||||
private val coreRpcFlows = listOf(
|
||||
ContractUpgradeFlow.Initiate::class.java,
|
||||
ContractUpgradeFlow.Authorise::class.java,
|
||||
ContractUpgradeFlow.Deauthorise::class.java
|
||||
)
|
||||
|
||||
/** A Cordapp representing the core package which is not scanned automatically. */
|
||||
fun generateCoreCordapp(versionInfo: VersionInfo): CordappImpl {
|
||||
return CordappImpl(
|
||||
contractClassNames = listOf(),
|
||||
initiatedFlows = listOf(),
|
||||
rpcFlows = coreRpcFlows,
|
||||
serviceFlows = listOf(),
|
||||
schedulableFlows = listOf(),
|
||||
services = listOf(),
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(),
|
||||
info = CordappImpl.Info("corda-core", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
|
||||
allFlows = listOf(),
|
||||
jarPath = ContractUpgradeFlow.javaClass.location, // Core JAR location
|
||||
jarHash = SecureHash.allOnesHash,
|
||||
notaryService = null,
|
||||
isLoaded = false
|
||||
)
|
||||
}
|
||||
|
||||
/** A Cordapp for the built-in notary service implementation. */
|
||||
fun generateSimpleNotaryCordapp(versionInfo: VersionInfo): CordappImpl {
|
||||
return CordappImpl(
|
||||
contractClassNames = listOf(),
|
||||
initiatedFlows = listOf(),
|
||||
rpcFlows = listOf(),
|
||||
serviceFlows = listOf(),
|
||||
schedulableFlows = listOf(),
|
||||
services = listOf(),
|
||||
serializationWhitelists = listOf(),
|
||||
serializationCustomSerializers = listOf(),
|
||||
customSchemas = setOf(NodeNotarySchemaV1),
|
||||
info = CordappImpl.Info("corda-notary", versionInfo.vendor, versionInfo.releaseVersion, 1, versionInfo.platformVersion),
|
||||
allFlows = listOf(),
|
||||
jarPath = SimpleNotaryService::class.java.location,
|
||||
jarHash = SecureHash.allOnesHash,
|
||||
notaryService = SimpleNotaryService::class.java,
|
||||
isLoaded = false
|
||||
)
|
||||
}
|
||||
}
|
@ -4,14 +4,13 @@ package net.corda.node.internal.security
|
||||
import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.google.common.primitives.Ints
|
||||
import net.corda.core.context.AuthServiceId
|
||||
import net.corda.core.internal.buildNamed
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.internal.DataSourceFactory
|
||||
import net.corda.node.services.config.AuthDataSourceType
|
||||
import net.corda.node.services.config.PasswordEncryption
|
||||
import net.corda.node.services.config.SecurityConfiguration
|
||||
import net.corda.node.services.config.AuthDataSourceType
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import org.apache.shiro.authc.*
|
||||
import org.apache.shiro.authc.credential.PasswordMatcher
|
||||
@ -28,22 +27,22 @@ import org.apache.shiro.realm.jdbc.JdbcRealm
|
||||
import org.apache.shiro.subject.PrincipalCollection
|
||||
import org.apache.shiro.subject.SimplePrincipalCollection
|
||||
import java.io.Closeable
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
|
||||
private typealias AuthServiceConfig = SecurityConfiguration.AuthService
|
||||
|
||||
/**
|
||||
* Default implementation of [RPCSecurityManager] adapting
|
||||
* [org.apache.shiro.mgt.SecurityManager]
|
||||
*/
|
||||
class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
|
||||
class RPCSecurityManagerImpl(config: AuthServiceConfig, cacheFactory: NamedCacheFactory) : RPCSecurityManager {
|
||||
|
||||
override val id = config.id
|
||||
private val manager: DefaultSecurityManager
|
||||
|
||||
init {
|
||||
manager = buildImpl(config)
|
||||
manager = buildImpl(config, cacheFactory)
|
||||
}
|
||||
|
||||
@Throws(FailedLoginException::class)
|
||||
@ -75,14 +74,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
|
||||
|
||||
private val logger = loggerFor<RPCSecurityManagerImpl>()
|
||||
|
||||
/**
|
||||
* Instantiate RPCSecurityManager initialised with users data from a list of [User]
|
||||
*/
|
||||
fun fromUserList(id: AuthServiceId, users: List<User>) =
|
||||
RPCSecurityManagerImpl(AuthServiceConfig.fromUsers(users).copy(id = id))
|
||||
|
||||
// Build internal Shiro securityManager instance
|
||||
private fun buildImpl(config: AuthServiceConfig): DefaultSecurityManager {
|
||||
private fun buildImpl(config: AuthServiceConfig, cacheFactory: NamedCacheFactory): DefaultSecurityManager {
|
||||
val realm = when (config.dataSource.type) {
|
||||
AuthDataSourceType.DB -> {
|
||||
logger.info("Constructing DB-backed security data source: ${config.dataSource.connection}")
|
||||
@ -98,7 +91,8 @@ class RPCSecurityManagerImpl(config: AuthServiceConfig) : RPCSecurityManager {
|
||||
it.cacheManager = config.options?.cache?.let {
|
||||
CaffeineCacheManager(
|
||||
timeToLiveSeconds = it.expireAfterSecs,
|
||||
maxSize = it.maxEntries)
|
||||
maxSize = it.maxEntries,
|
||||
cacheFactory = cacheFactory)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -294,7 +288,8 @@ private fun <K : Any, V> Cache<K, V>.toShiroCache() = object : ShiroCache<K, V>
|
||||
* cache implementation in [com.github.benmanes.caffeine.cache.Cache]
|
||||
*/
|
||||
private class CaffeineCacheManager(val maxSize: Long,
|
||||
val timeToLiveSeconds: Long) : CacheManager {
|
||||
val timeToLiveSeconds: Long,
|
||||
val cacheFactory: NamedCacheFactory) : CacheManager {
|
||||
|
||||
private val instances = ConcurrentHashMap<String, ShiroCache<*, *>>()
|
||||
|
||||
@ -306,11 +301,7 @@ private class CaffeineCacheManager(val maxSize: Long,
|
||||
|
||||
private fun <K : Any, V> buildCache(name: String): ShiroCache<K, V> {
|
||||
logger.info("Constructing cache '$name' with maximumSize=$maxSize, TTL=${timeToLiveSeconds}s")
|
||||
return Caffeine.newBuilder()
|
||||
.expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS)
|
||||
.maximumSize(maxSize)
|
||||
.buildNamed<K, V>("RPCSecurityManagerShiroCache_$name")
|
||||
.toShiroCache()
|
||||
return cacheFactory.buildNamed<K, V>(Caffeine.newBuilder(), "RPCSecurityManagerShiroCache_$name").toShiroCache()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -0,0 +1,14 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
|
||||
class ClearNetworkCacheCli(startup: NodeStartup): NodeCliCommand("clear-network-cache", "Clears local copy of network map, on node startup it will be restored from server or file system.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, object: RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) = node.clearNetworkMapCache()
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
|
||||
class GenerateNodeInfoCli(startup: NodeStartup): NodeCliCommand("generate-node-info", "Performs the node start-up tasks necessary to generate the nodeInfo file, saves it to disk, then exits.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, object : RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) {
|
||||
node.generateAndSaveNodeInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.internal.NodeCliCommand
|
||||
import net.corda.node.internal.NodeStartup
|
||||
import net.corda.node.internal.RunAfterNodeInitialisation
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.utilities.createKeyPairAndSelfSignedTLSCertificate
|
||||
import net.corda.node.utilities.saveToKeyStore
|
||||
import net.corda.node.utilities.saveToTrustStore
|
||||
import java.io.Console
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class GenerateRpcSslCertsCli(startup: NodeStartup): NodeCliCommand("generate-rpc-ssl-settings", "Generates the SSL key and trust stores for a secure RPC connection.", startup) {
|
||||
override fun runProgram(): Int {
|
||||
return startup.initialiseAndRun(cmdLineOptions, GenerateRpcSslCerts())
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateRpcSslCerts: RunAfterNodeInitialisation {
|
||||
override fun run(node: Node) {
|
||||
generateRpcSslCertificates(node.configuration)
|
||||
}
|
||||
|
||||
private fun generateRpcSslCertificates(conf: NodeConfiguration) {
|
||||
val (keyPair, cert) = createKeyPairAndSelfSignedTLSCertificate(conf.myLegalName.x500Principal)
|
||||
|
||||
val keyStorePath = conf.baseDirectory / "certificates" / "rpcsslkeystore.jks"
|
||||
val trustStorePath = conf.baseDirectory / "certificates" / "export" / "rpcssltruststore.jks"
|
||||
|
||||
if (keyStorePath.exists() || trustStorePath.exists()) {
|
||||
println("Found existing RPC SSL keystores. Command was already run. Exiting.")
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
val console: Console? = System.console()
|
||||
|
||||
when (console) {
|
||||
// In this case, the JVM is not connected to the console so we need to exit.
|
||||
null -> {
|
||||
println("Not connected to console. Exiting.")
|
||||
exitProcess(1)
|
||||
}
|
||||
// Otherwise we can proceed normally.
|
||||
else -> {
|
||||
while (true) {
|
||||
val keystorePassword1 = console.readPassword("Enter the RPC keystore password:")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (keystorePassword1.isEmpty()) {
|
||||
println("The RPC keystore password cannot be an empty String.")
|
||||
continue
|
||||
}
|
||||
|
||||
val keystorePassword2 = console.readPassword("Re-enter the RPC keystore password:")
|
||||
if (!keystorePassword1.contentEquals(keystorePassword2)) {
|
||||
println("The RPC keystore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToKeyStore(keyStorePath, keyPair, cert, String(keystorePassword1), "rpcssl")
|
||||
println("The RPC keystore was saved to: $keyStorePath .")
|
||||
break
|
||||
}
|
||||
|
||||
while (true) {
|
||||
val trustStorePassword1 = console.readPassword("Enter the RPC truststore password:")
|
||||
// TODO: consider adding a password strength policy.
|
||||
if (trustStorePassword1.isEmpty()) {
|
||||
println("The RPC truststore password cannot be an empty string.")
|
||||
continue
|
||||
}
|
||||
|
||||
val trustStorePassword2 = console.readPassword("Re-enter the RPC truststore password:")
|
||||
if (!trustStorePassword1.contentEquals(trustStorePassword2)) {
|
||||
println("The RPC truststore passwords don't match.")
|
||||
continue
|
||||
}
|
||||
|
||||
saveToTrustStore(trustStorePath, cert, String(trustStorePassword1), "rpcssl")
|
||||
println("The RPC truststore was saved to: $trustStorePath.")
|
||||
println("You need to distribute this file along with the password in a secure way to all RPC clients.")
|
||||
break
|
||||
}
|
||||
|
||||
val dollar = '$'
|
||||
println("""
|
||||
|
|
||||
|The SSL certificates for RPC were generated successfully.
|
||||
|
|
||||
|Add this snippet to the "rpcSettings" section of your node.conf:
|
||||
| useSsl=true
|
||||
| ssl {
|
||||
| keyStorePath=$dollar{baseDirectory}/certificates/rpcsslkeystore.jks
|
||||
| keyStorePassword=the_above_password
|
||||
| }
|
||||
|""".trimMargin())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package net.corda.node.internal.subcommands
|
||||
|
||||
import net.corda.cliutils.CliWrapperBase
|
||||
import net.corda.core.internal.createFile
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.exists
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.node.InitialRegistrationCmdLineOptions
|
||||
import net.corda.node.NodeRegistrationOption
|
||||
import net.corda.node.internal.*
|
||||
import net.corda.node.internal.NodeStartupLogging.Companion.logger
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.utilities.registration.HTTPNetworkRegistrationService
|
||||
import net.corda.node.utilities.registration.NodeRegistrationHelper
|
||||
import picocli.CommandLine.Mixin
|
||||
import picocli.CommandLine.Option
|
||||
import java.io.File
|
||||
import java.nio.file.Path
|
||||
|
||||
class InitialRegistrationCli(val startup: NodeStartup): CliWrapperBase("initial-registration", "Starts initial node registration with Corda network to obtain certificate from the permissioning server.") {
|
||||
@Option(names = ["-t", "--network-root-truststore"], description = ["Network root trust store obtained from network operator."])
|
||||
var networkRootTrustStorePathParameter: Path? = null
|
||||
|
||||
@Option(names = ["-p", "--network-root-truststore-password"], description = ["Network root trust store password obtained from network operator."], required = true)
|
||||
var networkRootTrustStorePassword: String = ""
|
||||
|
||||
override fun runProgram() : Int {
|
||||
val networkRootTrustStorePath: Path = networkRootTrustStorePathParameter ?: cmdLineOptions.baseDirectory / "certificates" / "network-root-truststore.jks"
|
||||
return startup.initialiseAndRun(cmdLineOptions, InitialRegistration(cmdLineOptions.baseDirectory, networkRootTrustStorePath, networkRootTrustStorePassword, startup))
|
||||
}
|
||||
|
||||
override fun initLogging() = this.initLogging(cmdLineOptions.baseDirectory)
|
||||
|
||||
@Mixin
|
||||
val cmdLineOptions = InitialRegistrationCmdLineOptions()
|
||||
}
|
||||
|
||||
class InitialRegistration(val baseDirectory: Path, private val networkRootTrustStorePath: Path, networkRootTrustStorePassword: String, private val startup: NodeStartup) : RunAfterNodeInitialisation, NodeStartupLogging {
|
||||
companion object {
|
||||
private const val INITIAL_REGISTRATION_MARKER = ".initialregistration"
|
||||
|
||||
fun checkRegistrationMode(baseDirectory: Path): Boolean {
|
||||
// If the node was started with `--initial-registration`, create marker file.
|
||||
// We do this here to ensure the marker is created even if parsing the args with NodeArgsParser fails.
|
||||
val marker = baseDirectory / INITIAL_REGISTRATION_MARKER
|
||||
if (!marker.exists()) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
marker.createFile()
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Could not create marker file for `initial-registration`.", e)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val nodeRegistration = NodeRegistrationOption(networkRootTrustStorePath, networkRootTrustStorePassword)
|
||||
|
||||
private fun registerWithNetwork(conf: NodeConfiguration) {
|
||||
val versionInfo = startup.getVersionInfo()
|
||||
|
||||
println("\n" +
|
||||
"******************************************************************\n" +
|
||||
"* *\n" +
|
||||
"* Registering as a new participant with a Corda network *\n" +
|
||||
"* *\n" +
|
||||
"******************************************************************\n")
|
||||
|
||||
NodeRegistrationHelper(conf,
|
||||
HTTPNetworkRegistrationService(
|
||||
requireNotNull(conf.networkServices),
|
||||
versionInfo),
|
||||
nodeRegistration).buildKeystore()
|
||||
|
||||
// Minimal changes to make registration tool create node identity.
|
||||
// TODO: Move node identity generation logic from node to registration helper.
|
||||
startup.createNode(conf, versionInfo).generateAndSaveNodeInfo()
|
||||
|
||||
println("Successfully registered Corda node with compatibility zone, node identity keys and certificates are stored in '${conf.certificatesDirectory}', it is advised to backup the private keys and certificates.")
|
||||
println("Corda node will now terminate.")
|
||||
}
|
||||
|
||||
private fun initialRegistration(config: NodeConfiguration) {
|
||||
// Null checks for [compatibilityZoneURL], [rootTruststorePath] and [rootTruststorePassword] has been done in [CmdLineOptions.loadConfig]
|
||||
attempt { registerWithNetwork(config) }.doOnException(this::handleRegistrationError) as? Try.Success
|
||||
// At this point the node registration was successful. We can delete the marker file.
|
||||
deleteNodeRegistrationMarker(baseDirectory)
|
||||
}
|
||||
|
||||
private fun deleteNodeRegistrationMarker(baseDir: Path) {
|
||||
try {
|
||||
val marker = File((baseDir / INITIAL_REGISTRATION_MARKER).toUri())
|
||||
if (marker.exists()) {
|
||||
marker.delete()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.logAsUnexpected( "Could not delete the marker file that was created for `initial-registration`.", print = logger::warn)
|
||||
}
|
||||
}
|
||||
|
||||
override fun run(node: Node) {
|
||||
require(networkRootTrustStorePath.exists()) { "Network root trust store path: '$networkRootTrustStorePath' doesn't exist" }
|
||||
if (checkRegistrationMode(baseDirectory)) {
|
||||
println("Node was started before with `--initial-registration`, but the registration was not completed.\nResuming registration.")
|
||||
}
|
||||
initialRegistration(node.configuration)
|
||||
}
|
||||
}
|
||||
|
@ -12,10 +12,9 @@ import com.esotericsoftware.kryo.serializers.ClosureSerializer
|
||||
import net.corda.core.internal.uncheckedCast
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationScheme
|
||||
import net.corda.core.serialization.internal.CheckpointSerializer
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.serialization.internal.*
|
||||
import java.security.PublicKey
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
val kryoMagic = CordaSerializationMagic("corda".toByteArray() + byteArrayOf(0, 0))
|
||||
@ -31,7 +30,7 @@ private object AutoCloseableSerialisationDetector : Serializer<AutoCloseable>()
|
||||
override fun read(kryo: Kryo, input: Input, type: Class<AutoCloseable>) = throw IllegalStateException("Should not reach here!")
|
||||
}
|
||||
|
||||
object KryoSerializationScheme : CheckpointSerializationScheme {
|
||||
object KryoCheckpointSerializer : CheckpointSerializer {
|
||||
private val kryoPoolsForContexts = ConcurrentHashMap<Pair<ClassWhitelist, ClassLoader>, KryoPool>()
|
||||
|
||||
private fun getPool(context: CheckpointSerializationContext): KryoPool {
|
@ -20,6 +20,8 @@ class FinalityHandler(private val sender: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
subFlow(ReceiveTransactionFlow(sender, true, StatesToRecord.ONLY_RELEVANT))
|
||||
}
|
||||
|
||||
internal fun sender(): Party = sender.counterparty
|
||||
}
|
||||
|
||||
class NotaryChangeHandler(otherSideSession: FlowSession) : AbstractStateReplacementFlow.Acceptor<Party>(otherSideSession) {
|
||||
@ -54,7 +56,7 @@ class ContractUpgradeHandler(otherSide: FlowSession) : AbstractStateReplacementF
|
||||
// verify outputs matches the proposed upgrade.
|
||||
val ourSTX = serviceHub.validatedTransactions.getTransaction(proposal.stateRef.txhash)
|
||||
requireNotNull(ourSTX) { "We don't have a copy of the referenced state" }
|
||||
val oldStateAndRef = ourSTX!!.tx.outRef<ContractState>(proposal.stateRef.index)
|
||||
val oldStateAndRef = ourSTX!!.resolveBaseTransaction(serviceHub).outRef<ContractState>(proposal.stateRef.index)
|
||||
val authorisedUpgrade = serviceHub.contractUpgradeService.getAuthorisedContractUpgrade(oldStateAndRef.ref) ?: throw IllegalStateException("Contract state upgrade is unauthorised. State hash : ${oldStateAndRef.ref}")
|
||||
val proposedTx = stx.coreTransaction as ContractUpgradeWireTransaction
|
||||
val expectedTx = ContractUpgradeUtils.assembleUpgradeTx(oldStateAndRef, proposal.modification, proposedTx.privacySalt, serviceHub)
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.internal.FlowStateMachine
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.messaging.StateMachineTransactionMapping
|
||||
@ -25,7 +26,6 @@ import net.corda.node.services.network.NetworkMapUpdater
|
||||
import net.corda.node.services.persistence.AttachmentStorageInternal
|
||||
import net.corda.node.services.statemachine.ExternalEvent
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import java.security.PublicKey
|
||||
|
||||
|
@ -86,9 +86,9 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N
|
||||
}
|
||||
|
||||
if (keyStore.getOptional() == null || signingCertificateStore.getOptional() == null) {
|
||||
val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.password).get(true).also { it.registerDevSigningCertificates(myLegalName) }
|
||||
val signingKeyStore = FileBasedCertificateStoreSupplier(signingCertificateStore.path, signingCertificateStore.storePassword, signingCertificateStore.entryPassword).get(true).also { it.registerDevSigningCertificates(myLegalName) }
|
||||
|
||||
FileBasedCertificateStoreSupplier(keyStore.path, keyStore.password).get(true).also { it.registerDevP2pCertificates(myLegalName) }
|
||||
FileBasedCertificateStoreSupplier(keyStore.path, keyStore.storePassword, keyStore.entryPassword).get(true).also { it.registerDevP2pCertificates(myLegalName) }
|
||||
|
||||
// Move distributed service composite key (generated by IdentityGenerator.generateToDisk) to keystore if exists.
|
||||
val distributedServiceKeystore = certificatesDirectory / "distributedService.jks"
|
||||
@ -97,7 +97,7 @@ fun MutualSslConfiguration.configureDevKeyAndTrustStores(myLegalName: CordaX500N
|
||||
signingKeyStore.update {
|
||||
serviceKeystore.aliases().forEach {
|
||||
if (serviceKeystore.internal.isKeyEntry(it)) {
|
||||
setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_PRIVATE_KEY_PASS), serviceKeystore.getCertificateChain(it))
|
||||
setPrivateKey(it, serviceKeystore.getPrivateKey(it, DEV_CA_KEY_STORE_PASS), serviceKeystore.getCertificateChain(it), signingKeyStore.entryPassword)
|
||||
} else {
|
||||
setCertificate(it, serviceKeystore.getCertificate(it))
|
||||
}
|
||||
|
@ -11,12 +11,7 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.services.config.rpc.NodeRpcOptions
|
||||
import net.corda.nodeapi.BrokerRpcSslOptions
|
||||
import net.corda.nodeapi.internal.config.FileBasedCertificateStoreSupplier
|
||||
import net.corda.nodeapi.internal.config.SslConfiguration
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import net.corda.nodeapi.internal.config.User
|
||||
import net.corda.nodeapi.internal.config.parseAs
|
||||
import net.corda.nodeapi.internal.config.*
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.tools.shell.SSHDConfiguration
|
||||
import org.slf4j.Logger
|
||||
@ -73,7 +68,7 @@ interface NodeConfiguration {
|
||||
val flowMonitorPeriodMillis: Duration get() = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS
|
||||
val flowMonitorSuspensionLoggingThresholdMillis: Duration get() = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS
|
||||
val crlCheckSoftFail: Boolean
|
||||
val jmxReporterType : JmxReporterType? get() = defaultJmxReporterType
|
||||
val jmxReporterType: JmxReporterType? get() = defaultJmxReporterType
|
||||
|
||||
val baseDirectory: Path
|
||||
val certificatesDirectory: Path
|
||||
@ -81,6 +76,7 @@ interface NodeConfiguration {
|
||||
val p2pSslOptions: MutualSslConfiguration
|
||||
|
||||
val cordappDirectories: List<Path>
|
||||
val flowOverrides: FlowOverrideConfig?
|
||||
|
||||
fun validate(): List<String>
|
||||
|
||||
@ -102,6 +98,9 @@ interface NodeConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
data class FlowOverrideConfig(val overrides: List<FlowOverride> = listOf())
|
||||
data class FlowOverride(val initiator: String, val responder: String)
|
||||
|
||||
/**
|
||||
* Currently registered JMX Reporters
|
||||
*/
|
||||
@ -119,34 +118,16 @@ fun NodeConfiguration.shouldStartSSHDaemon() = this.sshd != null
|
||||
fun NodeConfiguration.shouldStartLocalShell() = !this.noLocalShell && System.console() != null && this.devMode
|
||||
fun NodeConfiguration.shouldInitCrashShell() = shouldStartLocalShell() || shouldStartSSHDaemon()
|
||||
|
||||
data class NotaryConfig(val validating: Boolean,
|
||||
val raft: RaftConfig? = null,
|
||||
val bftSMaRt: BFTSMaRtConfiguration? = null,
|
||||
val custom: Boolean = false,
|
||||
val serviceLegalName: CordaX500Name? = null
|
||||
) {
|
||||
init {
|
||||
require(raft == null || bftSMaRt == null || !custom) {
|
||||
"raft, bftSMaRt, and custom configs cannot be specified together"
|
||||
}
|
||||
}
|
||||
|
||||
val isClusterConfig: Boolean get() = raft != null || bftSMaRt != null
|
||||
}
|
||||
|
||||
data class RaftConfig(val nodeAddress: NetworkHostAndPort, val clusterAddresses: List<NetworkHostAndPort>)
|
||||
|
||||
/** @param exposeRaces for testing only, so its default is not in reference.conf but here. */
|
||||
data class BFTSMaRtConfiguration(
|
||||
val replicaId: Int,
|
||||
val clusterAddresses: List<NetworkHostAndPort>,
|
||||
val debug: Boolean = false,
|
||||
val exposeRaces: Boolean = false
|
||||
) {
|
||||
init {
|
||||
require(replicaId >= 0) { "replicaId cannot be negative" }
|
||||
}
|
||||
}
|
||||
data class NotaryConfig(
|
||||
/** Specifies whether the notary validates transactions or not. */
|
||||
val validating: Boolean,
|
||||
/** The legal name of cluster in case of a distributed notary service. */
|
||||
val serviceLegalName: CordaX500Name? = null,
|
||||
/** The name of the notary service class to load. */
|
||||
val className: String = "net.corda.node.services.transactions.SimpleNotaryService",
|
||||
/** Notary implementation-specific configuration parameters. */
|
||||
val extraConfig: Config? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Used as an alternative to the older compatibilityZoneURL to allow the doorman and network map
|
||||
@ -156,6 +137,8 @@ data class BFTSMaRtConfiguration(
|
||||
*
|
||||
* @property doormanURL The URL of the tls certificate signing service.
|
||||
* @property networkMapURL The URL of the Network Map service.
|
||||
* @property pnm If the compatibility zone operator supports the private network map option, have the node
|
||||
* at registration automatically join that private network.
|
||||
* @property inferred Non user setting that indicates weather the Network Services configuration was
|
||||
* set explicitly ([inferred] == false) or weather they have been inferred via the compatibilityZoneURL parameter
|
||||
* ([inferred] == true) where both the network map and doorman are running on the same endpoint. Only one,
|
||||
@ -164,7 +147,8 @@ data class BFTSMaRtConfiguration(
|
||||
data class NetworkServicesConfig(
|
||||
val doormanURL: URL,
|
||||
val networkMapURL: URL,
|
||||
val inferred : Boolean = false
|
||||
val pnm: UUID? = null,
|
||||
val inferred: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
@ -230,7 +214,8 @@ data class NodeConfigurationImpl(
|
||||
override val flowMonitorPeriodMillis: Duration = DEFAULT_FLOW_MONITOR_PERIOD_MILLIS,
|
||||
override val flowMonitorSuspensionLoggingThresholdMillis: Duration = DEFAULT_FLOW_MONITOR_SUSPENSION_LOGGING_THRESHOLD_MILLIS,
|
||||
override val cordappDirectories: List<Path> = listOf(baseDirectory / CORDAPPS_DIR_NAME_DEFAULT),
|
||||
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA
|
||||
override val jmxReporterType: JmxReporterType? = JmxReporterType.JOLOKIA,
|
||||
override val flowOverrides: FlowOverrideConfig?
|
||||
) : NodeConfiguration {
|
||||
companion object {
|
||||
private val logger = loggerFor<NodeConfigurationImpl>()
|
||||
@ -257,12 +242,16 @@ data class NodeConfigurationImpl(
|
||||
override val certificatesDirectory = baseDirectory / "certificates"
|
||||
|
||||
private val signingCertificateStorePath = certificatesDirectory / "nodekeystore.jks"
|
||||
override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword)
|
||||
|
||||
private val p2pKeystorePath: Path get() = certificatesDirectory / "sslkeystore.jks"
|
||||
private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword)
|
||||
|
||||
// TODO: There are two implications here:
|
||||
// 1. "signingCertificateStore" and "p2pKeyStore" have the same passwords. In the future we should re-visit this "rule" and see of they can be made different;
|
||||
// 2. The passwords for store and for keys in this store are the same, this is due to limitations of Artemis.
|
||||
override val signingCertificateStore = FileBasedCertificateStoreSupplier(signingCertificateStorePath, keyStorePassword, keyStorePassword)
|
||||
private val p2pKeyStore = FileBasedCertificateStoreSupplier(p2pKeystorePath, keyStorePassword, keyStorePassword)
|
||||
|
||||
private val p2pTrustStoreFilePath: Path get() = certificatesDirectory / "truststore.jks"
|
||||
private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword)
|
||||
private val p2pTrustStore = FileBasedCertificateStoreSupplier(p2pTrustStoreFilePath, trustStorePassword, trustStorePassword)
|
||||
override val p2pSslOptions: MutualSslConfiguration = SslConfiguration.mutual(p2pKeyStore, p2pTrustStore)
|
||||
|
||||
override val rpcOptions: NodeRpcOptions
|
||||
@ -353,7 +342,7 @@ data class NodeConfigurationImpl(
|
||||
|
||||
override val effectiveH2Settings: NodeH2Settings?
|
||||
get() = when {
|
||||
h2port != null -> NodeH2Settings(address = NetworkHostAndPort(host="localhost", port=h2port))
|
||||
h2port != null -> NodeH2Settings(address = NetworkHostAndPort(host = "localhost", port = h2port))
|
||||
else -> h2Settings
|
||||
}
|
||||
|
||||
@ -365,14 +354,15 @@ data class NodeConfigurationImpl(
|
||||
"Cannot specify both 'rpcUsers' and 'security' in configuration"
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
if(certificateChainCheckPolicies.isNotEmpty()) {
|
||||
if (certificateChainCheckPolicies.isNotEmpty()) {
|
||||
logger.warn("""You are configuring certificateChainCheckPolicies. This is a setting that is not used, and will be removed in a future version.
|
||||
|Please contact the R3 team on the public slack to discuss your use case.
|
||||
""".trimMargin())
|
||||
}
|
||||
|
||||
// Support the deprecated method of configuring network services with a single compatibilityZoneURL option
|
||||
if (compatibilityZoneURL != null && networkServices == null) {
|
||||
networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, true)
|
||||
networkServices = NetworkServicesConfig(compatibilityZoneURL, compatibilityZoneURL, inferred = true)
|
||||
}
|
||||
require(h2port == null || h2Settings == null) { "Cannot specify both 'h2port' and 'h2Settings' in configuration" }
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.identity
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.*
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.hash
|
||||
import net.corda.core.node.services.UnknownAnonymousPartyException
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -10,7 +11,6 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.IdentityServiceInternal
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import net.corda.nodeapi.internal.crypto.x509Certificates
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
|
@ -2,11 +2,11 @@ package net.corda.node.services.keys
|
||||
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.utilities.MAX_HASH_HEX_SIZE
|
||||
import net.corda.node.services.identity.PersistentIdentityService
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import org.apache.commons.lang.ArrayUtils.EMPTY_BYTE_ARRAY
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.internal.nodeSerializationEnv
|
||||
@ -20,7 +21,7 @@ class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serv
|
||||
private var locator: ServerLocator? = null
|
||||
private var rpcServer: RPCServer? = null
|
||||
|
||||
fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager) = synchronized(this) {
|
||||
fun init(rpcOps: RPCOps, securityManager: RPCSecurityManager, cacheFactory: NamedCacheFactory) = synchronized(this) {
|
||||
|
||||
val tcpTransport = ArtemisTcpTransport.rpcInternalClientTcpTransport(serverAddress, sslConfig)
|
||||
locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply {
|
||||
@ -32,7 +33,7 @@ class InternalRPCMessagingClient(val sslConfig: MutualSslConfiguration, val serv
|
||||
isUseGlobalPools = nodeSerializationEnv != null
|
||||
}
|
||||
|
||||
rpcServer = RPCServer(rpcOps, NODE_RPC_USER, NODE_RPC_USER, locator!!, securityManager, nodeName, rpcServerConfiguration)
|
||||
rpcServer = RPCServer(rpcOps, NODE_RPC_USER, NODE_RPC_USER, locator!!, securityManager, nodeName, rpcServerConfiguration, cacheFactory)
|
||||
}
|
||||
|
||||
fun start(serverControl: ActiveMQServerControl) = synchronized(this) {
|
||||
|
@ -2,9 +2,9 @@ package net.corda.node.services.messaging
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.node.services.statemachine.DeduplicationId
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import java.time.Instant
|
||||
|
@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import net.corda.core.crypto.toStringShort
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
@ -27,7 +28,6 @@ import net.corda.node.services.statemachine.DeduplicationId
|
||||
import net.corda.node.services.statemachine.ExternalEvent
|
||||
import net.corda.node.services.statemachine.SenderDeduplicationId
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.*
|
||||
import net.corda.nodeapi.internal.ArtemisMessagingComponent.Companion.BRIDGE_CONTROL
|
||||
|
@ -13,7 +13,7 @@ import net.corda.core.context.Trace
|
||||
import net.corda.core.context.Trace.InvocationId
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.internal.LifeCycle
|
||||
import net.corda.core.internal.buildNamed
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.serialization.SerializationContext
|
||||
import net.corda.core.serialization.SerializationDefaults
|
||||
@ -33,13 +33,8 @@ import net.corda.nodeapi.internal.persistence.contextDatabase
|
||||
import net.corda.nodeapi.internal.persistence.contextDatabaseOrNull
|
||||
import org.apache.activemq.artemis.api.core.Message
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.api.core.client.*
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient.DEFAULT_ACK_BATCH_SIZE
|
||||
import org.apache.activemq.artemis.api.core.client.ClientConsumer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientMessage
|
||||
import org.apache.activemq.artemis.api.core.client.ClientProducer
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSessionFactory
|
||||
import org.apache.activemq.artemis.api.core.client.ServerLocator
|
||||
import org.apache.activemq.artemis.api.core.management.ActiveMQServerControl
|
||||
import org.apache.activemq.artemis.api.core.management.CoreNotificationType
|
||||
import org.apache.activemq.artemis.api.core.management.ManagementHelper
|
||||
@ -49,12 +44,7 @@ import java.lang.reflect.InvocationTargetException
|
||||
import java.lang.reflect.Method
|
||||
import java.time.Duration
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.*
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
private typealias ObservableSubscriptionMap = Cache<InvocationId, ObservableSubscription>
|
||||
@ -91,7 +81,8 @@ class RPCServer(
|
||||
private val serverLocator: ServerLocator,
|
||||
private val securityManager: RPCSecurityManager,
|
||||
private val nodeLegalName: CordaX500Name,
|
||||
private val rpcConfiguration: RPCServerConfiguration
|
||||
private val rpcConfiguration: RPCServerConfiguration,
|
||||
private val cacheFactory: NamedCacheFactory
|
||||
) {
|
||||
private companion object {
|
||||
private val log = contextLogger()
|
||||
@ -136,7 +127,7 @@ class RPCServer(
|
||||
private val responseMessageBuffer = ConcurrentHashMap<SimpleString, BufferOrNone>()
|
||||
private val sendJobQueue = LinkedBlockingQueue<RpcSendJob>()
|
||||
|
||||
private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry)
|
||||
private val deduplicationChecker = DeduplicationChecker(rpcConfiguration.deduplicationCacheExpiry, cacheFactory = cacheFactory)
|
||||
private var deduplicationIdentity: String? = null
|
||||
|
||||
init {
|
||||
@ -154,7 +145,7 @@ class RPCServer(
|
||||
log.debug { "Unsubscribing from Observable with id $key because of $cause" }
|
||||
value!!.subscription.unsubscribe()
|
||||
}
|
||||
return Caffeine.newBuilder().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()).buildNamed("RPCServer_observableSubscription")
|
||||
return cacheFactory.buildNamed(Caffeine.newBuilder().removalListener(onObservableRemove).executor(SameThreadExecutor.getExecutor()), "RPCServer_observableSubscription")
|
||||
}
|
||||
|
||||
fun start(activeMqServerControl: ActiveMQServerControl) {
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.identity.PartyAndCertificate
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
@ -23,7 +24,6 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.internal.schemas.NodeInfoSchemaV1
|
||||
import net.corda.node.services.api.NetworkMapCacheInternal
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.NonInvalidatingCache
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.bufferUntilDatabaseCommit
|
||||
|
@ -3,6 +3,7 @@ package net.corda.node.services.persistence
|
||||
import net.corda.core.concurrent.CordaFuture
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.TransactionSignature
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.ThreadBox
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.bufferUntilSubscribed
|
||||
@ -15,7 +16,6 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.api.WritableTransactionStorage
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMapBase
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.WeightBasedAppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
|
@ -22,7 +22,6 @@ import net.corda.core.node.services.vault.AttachmentSort
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.node.services.vault.HibernateAttachmentQueryCriteriaParser
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.node.utilities.NonInvalidatingCache
|
||||
import net.corda.node.utilities.NonInvalidatingWeightBasedCache
|
||||
import net.corda.nodeapi.exceptions.DuplicateAttachmentException
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.NodePropertiesStore
|
||||
@ -17,12 +18,12 @@ import javax.persistence.Table
|
||||
/**
|
||||
* Simple node properties key value store in DB.
|
||||
*/
|
||||
class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database: CordaPersistence) : NodePropertiesStore {
|
||||
class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database: CordaPersistence, cacheFactory: NamedCacheFactory) : NodePropertiesStore {
|
||||
private companion object {
|
||||
val logger = contextLogger()
|
||||
}
|
||||
|
||||
override val flowsDrainingMode = FlowsDrainingModeOperationsImpl(readPhysicalNodeId, database, logger)
|
||||
override val flowsDrainingMode = FlowsDrainingModeOperationsImpl(readPhysicalNodeId, database, logger, cacheFactory)
|
||||
|
||||
fun start() {
|
||||
flowsDrainingMode.map.preload()
|
||||
@ -40,7 +41,7 @@ class NodePropertiesPersistentStore(readPhysicalNodeId: () -> String, database:
|
||||
)
|
||||
}
|
||||
|
||||
class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private val persistence: CordaPersistence, logger: Logger) : FlowsDrainingModeOperations {
|
||||
class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private val persistence: CordaPersistence, logger: Logger, cacheFactory: NamedCacheFactory) : FlowsDrainingModeOperations {
|
||||
private val nodeSpecificFlowsExecutionModeKey = "${readPhysicalNodeId()}_flowsExecutionMode"
|
||||
|
||||
init {
|
||||
@ -52,7 +53,8 @@ class FlowsDrainingModeOperationsImpl(readPhysicalNodeId: () -> String, private
|
||||
{ key -> key },
|
||||
{ entity -> entity.key to entity.value!! },
|
||||
NodePropertiesPersistentStore::DBNodeProperty,
|
||||
NodePropertiesPersistentStore.DBNodeProperty::class.java
|
||||
NodePropertiesPersistentStore.DBNodeProperty::class.java,
|
||||
cacheFactory
|
||||
)
|
||||
|
||||
override val values = PublishSubject.create<Pair<Boolean, Boolean>>()!!
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.schema
|
||||
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.FungibleAsset
|
||||
import net.corda.core.contracts.FungibleState
|
||||
import net.corda.core.contracts.LinearState
|
||||
import net.corda.core.schemas.*
|
||||
import net.corda.core.schemas.MappedSchemaValidator.crossReferencesToOtherMappedSchema
|
||||
@ -16,9 +17,7 @@ import net.corda.node.services.messaging.P2PMessageDeduplicator
|
||||
import net.corda.node.services.persistence.DBCheckpointStorage
|
||||
import net.corda.node.services.persistence.DBTransactionStorage
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.node.services.transactions.BFTNonValidatingNotaryService
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.transactions.RaftUniquenessProvider
|
||||
import net.corda.node.services.upgrade.ContractUpgradeServiceImpl
|
||||
import net.corda.node.services.vault.VaultSchemaV1
|
||||
|
||||
@ -29,7 +28,7 @@ import net.corda.node.services.vault.VaultSchemaV1
|
||||
* TODO: support plugins for schema version upgrading or custom mapping not supported by original [QueryableState].
|
||||
* TODO: create whitelisted tables when a CorDapp is first installed
|
||||
*/
|
||||
class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet(), includeNotarySchemas: Boolean = false) : SchemaService, SingletonSerializeAsToken() {
|
||||
class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()) : SchemaService, SingletonSerializeAsToken() {
|
||||
// Core Entities used by a Node
|
||||
object NodeCore
|
||||
|
||||
@ -47,26 +46,12 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
|
||||
override val migrationResource = "node-core.changelog-master"
|
||||
}
|
||||
|
||||
// Entities used by a Notary
|
||||
object NodeNotary
|
||||
|
||||
object NodeNotaryV1 : MappedSchema(schemaFamily = NodeNotary.javaClass, version = 1,
|
||||
mappedTypes = listOf(PersistentUniquenessProvider.BaseComittedState::class.java,
|
||||
PersistentUniquenessProvider.Request::class.java,
|
||||
PersistentUniquenessProvider.CommittedState::class.java,
|
||||
RaftUniquenessProvider.CommittedState::class.java,
|
||||
BFTNonValidatingNotaryService.CommittedState::class.java
|
||||
)) {
|
||||
override val migrationResource = "node-notary.changelog-master"
|
||||
}
|
||||
|
||||
// Required schemas are those used by internal Corda services
|
||||
private val requiredSchemas: Map<MappedSchema, SchemaService.SchemaOptions> =
|
||||
mapOf(Pair(CommonSchemaV1, SchemaOptions()),
|
||||
Pair(VaultSchemaV1, SchemaOptions()),
|
||||
Pair(NodeInfoSchemaV1, SchemaOptions()),
|
||||
Pair(NodeCoreV1, SchemaOptions())) +
|
||||
if (includeNotarySchemas) mapOf(Pair(NodeNotaryV1, SchemaOptions())) else emptyMap()
|
||||
Pair(NodeCoreV1, SchemaOptions()))
|
||||
|
||||
fun internalSchemas() = requiredSchemas.keys + extraSchemas.filter { schema -> // when mapped schemas from the finance module are present, they are considered as internal ones
|
||||
schema::class.qualifiedName == "net.corda.finance.schemas.CashSchemaV1" || schema::class.qualifiedName == "net.corda.finance.schemas.CommercialPaperSchemaV1" }
|
||||
@ -81,6 +66,8 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
|
||||
if (state is LinearState)
|
||||
schemas += VaultSchemaV1 // VaultLinearStates
|
||||
if (state is FungibleAsset<*>)
|
||||
schemas += VaultSchemaV1 // VaultFungibleAssets
|
||||
if (state is FungibleState<*>)
|
||||
schemas += VaultSchemaV1 // VaultFungibleStates
|
||||
|
||||
return schemas
|
||||
@ -92,6 +79,14 @@ class NodeSchemaService(private val extraSchemas: Set<MappedSchema> = emptySet()
|
||||
return VaultSchemaV1.VaultLinearStates(state.linearId, state.participants)
|
||||
if ((schema === VaultSchemaV1) && (state is FungibleAsset<*>))
|
||||
return VaultSchemaV1.VaultFungibleStates(state.owner, state.amount.quantity, state.amount.token.issuer.party, state.amount.token.issuer.reference, state.participants)
|
||||
if ((schema === VaultSchemaV1) && (state is FungibleState<*>))
|
||||
return VaultSchemaV1.VaultFungibleStates(
|
||||
participants = state.participants.toMutableSet(),
|
||||
owner = null,
|
||||
quantity = state.amount.quantity,
|
||||
issuer = null,
|
||||
issuerRef = null
|
||||
)
|
||||
return (state as QueryableState).generateMappedObject(schema)
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,7 @@ sealed class Action {
|
||||
/**
|
||||
* Execute the specified [operation].
|
||||
*/
|
||||
data class ExecuteAsyncOperation(val operation: FlowAsyncOperation<*>) : Action()
|
||||
data class ExecuteAsyncOperation(val deduplicationId: String, val operation: FlowAsyncOperation<*>) : Action()
|
||||
|
||||
/**
|
||||
* Release soft locks associated with given ID (currently the flow ID).
|
||||
|
@ -221,7 +221,7 @@ class ActionExecutorImpl(
|
||||
|
||||
@Suspendable
|
||||
private fun executeAsyncOperation(fiber: FlowFiber, action: Action.ExecuteAsyncOperation) {
|
||||
val operationFuture = action.operation.execute()
|
||||
val operationFuture = action.operation.execute(action.deduplicationId)
|
||||
operationFuture.thenMatch(
|
||||
success = { result ->
|
||||
fiber.scheduleEvent(Event.AsyncOperationCompletion(result))
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationContext
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.Try
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.trace
|
||||
@ -205,6 +206,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
|
||||
@Suspendable
|
||||
override fun run() {
|
||||
logic.progressTracker?.currentStep = ProgressTracker.STARTING
|
||||
logic.stateMachine = this
|
||||
|
||||
setLoggingContext()
|
||||
@ -218,7 +220,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
suspend(FlowIORequest.WaitForSessionConfirmations, maySkipCheckpoint = true)
|
||||
Try.Success(result)
|
||||
} catch (throwable: Throwable) {
|
||||
logger.info("Flow threw exception... sending to flow hospital", throwable)
|
||||
logger.info("Flow threw exception... sending it to flow hospital", throwable)
|
||||
Try.Failure<R>(throwable)
|
||||
}
|
||||
val softLocksId = if (hasSoftLockedStates) logic.runId.uuid else null
|
||||
@ -263,7 +265,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
processEventImmediately(
|
||||
Event.EnterSubFlow(subFlow.javaClass,
|
||||
createSubFlowVersion(
|
||||
serviceHub.cordappProvider.getCordappForFlow(subFlow), serviceHub.myInfo.platformVersion
|
||||
serviceHub.cordappProvider.getCordappForFlow(subFlow), serviceHub.myInfo.platformVersion
|
||||
)
|
||||
),
|
||||
isDbTransactionOpenOnEntry = true,
|
||||
@ -435,7 +437,7 @@ val Class<out FlowLogic<*>>.flowVersionAndInitiatingClass: Pair<Int, Class<out F
|
||||
current = current.superclass
|
||||
?: return found
|
||||
?: throw IllegalArgumentException("$name, as a flow that initiates other flows, must be annotated with " +
|
||||
"${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.")
|
||||
"${InitiatingFlow::class.java.name}. See https://docs.corda.net/api-flows.html#flowlogic-annotations.")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -127,7 +127,7 @@ class SingleThreadedStateMachineManager(
|
||||
override fun start(tokenizableServices: List<Any>) {
|
||||
checkQuasarJavaAgentPresence()
|
||||
val checkpointSerializationContext = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZATION_FACTORY, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
CheckpointSerializeAsTokenContextImpl(tokenizableServices, CheckpointSerializationDefaults.CHECKPOINT_SERIALIZER, CheckpointSerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
|
||||
)
|
||||
this.checkpointSerializationContext = checkpointSerializationContext
|
||||
this.actionExecutor = makeActionExecutor(checkpointSerializationContext)
|
||||
|
@ -217,7 +217,11 @@ class StaffedFlowHospital {
|
||||
*/
|
||||
object FinalityDoctor : Staff {
|
||||
override fun consult(flowFiber: FlowFiber, currentState: StateMachineState, newError: Throwable, history: MedicalHistory): Diagnosis {
|
||||
return if (currentState.flowLogic is FinalityHandler) Diagnosis.OVERNIGHT_OBSERVATION else Diagnosis.NOT_MY_SPECIALTY
|
||||
return (currentState.flowLogic as? FinalityHandler)?.let { logic -> Diagnosis.OVERNIGHT_OBSERVATION.also { warn(logic, flowFiber, currentState) } } ?: Diagnosis.NOT_MY_SPECIALTY
|
||||
}
|
||||
|
||||
private fun warn(flowLogic: FinalityHandler, flowFiber: FlowFiber, currentState: StateMachineState) {
|
||||
log.warn("Flow ${flowFiber.id} failed to be finalised. Manual intervention may be required before retrying the flow by re-starting the node. State machine state: $currentState, initiating party was: ${flowLogic.sender().name}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -411,7 +411,10 @@ class StartedFlowTransition(
|
||||
|
||||
private fun executeAsyncOperation(flowIORequest: FlowIORequest.ExecuteAsyncOperation<*>): TransitionResult {
|
||||
return builder {
|
||||
actions.add(Action.ExecuteAsyncOperation(flowIORequest.operation))
|
||||
// The `numberOfSuspends` is added to the deduplication ID in case an async
|
||||
// operation is executed multiple times within the same flow.
|
||||
val deduplicationId = context.id.toString() + ":" + currentState.checkpoint.numberOfSuspends.toString()
|
||||
actions.add(Action.ExecuteAsyncOperation(deduplicationId, flowIORequest.operation))
|
||||
FlowContinuation.ProcessEvents
|
||||
}
|
||||
}
|
||||
|
@ -1,174 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.flows.*
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.notary.NotaryInternalException
|
||||
import net.corda.core.internal.notary.NotaryService
|
||||
import net.corda.core.internal.notary.verifySignature
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.CoreTransaction
|
||||
import net.corda.core.transactions.FilteredTransaction
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.core.utilities.unwrap
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.config.BFTSMaRtConfiguration
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import java.security.PublicKey
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
/**
|
||||
* A non-validating notary service operated by a group of parties that don't necessarily trust each other.
|
||||
*
|
||||
* A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity.
|
||||
*/
|
||||
class BFTNonValidatingNotaryService(
|
||||
override val services: ServiceHubInternal,
|
||||
override val notaryIdentityKey: PublicKey,
|
||||
private val bftSMaRtConfig: BFTSMaRtConfiguration,
|
||||
cluster: BFTSMaRt.Cluster
|
||||
) : NotaryService() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
private val client: BFTSMaRt.Client
|
||||
private val replicaHolder = SettableFuture.create<Replica>()
|
||||
|
||||
init {
|
||||
client = BFTSMaRtConfig(bftSMaRtConfig.clusterAddresses, bftSMaRtConfig.debug, bftSMaRtConfig.exposeRaces).use {
|
||||
val replicaId = bftSMaRtConfig.replicaId
|
||||
val configHandle = it.handle()
|
||||
// Replica startup must be in parallel with other replicas, otherwise the constructor may not return:
|
||||
thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) {
|
||||
configHandle.use {
|
||||
val replica = Replica(it, replicaId, { createMap() }, services, notaryIdentityKey)
|
||||
replicaHolder.set(replica)
|
||||
log.info("BFT SMaRt replica $replicaId is running.")
|
||||
}
|
||||
}
|
||||
BFTSMaRt.Client(it, replicaId, cluster, this)
|
||||
}
|
||||
}
|
||||
|
||||
fun waitUntilReplicaHasInitialized() {
|
||||
log.debug { "Waiting for replica ${bftSMaRtConfig.replicaId} to initialize." }
|
||||
replicaHolder.getOrThrow() // It's enough to wait for the ServiceReplica constructor to return.
|
||||
}
|
||||
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party) = client.commitTransaction(payload, otherSide)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): FlowLogic<Void?> = ServiceFlow(otherPartySession, this)
|
||||
|
||||
private class ServiceFlow(val otherSideSession: FlowSession, val service: BFTNonValidatingNotaryService) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call(): Void? {
|
||||
val payload = otherSideSession.receive<NotarisationPayload>().unwrap { it }
|
||||
val response = commit(payload)
|
||||
otherSideSession.send(response)
|
||||
return null
|
||||
}
|
||||
|
||||
private fun commit(payload: NotarisationPayload): NotarisationResponse {
|
||||
val response = service.commitTransaction(payload, otherSideSession.counterparty)
|
||||
when (response) {
|
||||
is BFTSMaRt.ClusterResponse.Error -> {
|
||||
// TODO: here we assume that all error will be the same, but there might be invalid onces from mailicious nodes
|
||||
val responseError = response.errors.first().verified()
|
||||
throw NotaryException(responseError, payload.coreTransaction.id)
|
||||
}
|
||||
is BFTSMaRt.ClusterResponse.Signatures -> {
|
||||
log.debug("All input states of transaction ${payload.coreTransaction.id} have been committed")
|
||||
return NotarisationResponse(response.txSignatures)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}bft_committed_states")
|
||||
class CommittedState(id: PersistentStateRef, consumingTxHash: String) : PersistentUniquenessProvider.BaseComittedState(id, consumingTxHash)
|
||||
|
||||
private fun createMap(): AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef> {
|
||||
return AppendOnlyPersistentMap(
|
||||
cacheFactory = services.cacheFactory,
|
||||
name = "BFTNonValidatingNotaryService_transactions",
|
||||
toPersistentEntityKey = { PersistentStateRef(it.txhash.toString(), it.index) },
|
||||
fromPersistentEntity = {
|
||||
//TODO null check will become obsolete after making DB/JPA columns not nullable
|
||||
val txId = it.id.txId
|
||||
val index = it.id.index
|
||||
Pair(
|
||||
StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||
SecureHash.parse(it.consumingTxHash)
|
||||
)
|
||||
},
|
||||
toPersistentEntity = { (txHash, index): StateRef, id: SecureHash ->
|
||||
CommittedState(
|
||||
id = PersistentStateRef(txHash.toString(), index),
|
||||
consumingTxHash = id.toString()
|
||||
)
|
||||
},
|
||||
persistentEntityClass = CommittedState::class.java
|
||||
)
|
||||
}
|
||||
|
||||
private class Replica(config: BFTSMaRtConfig,
|
||||
replicaId: Int,
|
||||
createMap: () -> AppendOnlyPersistentMap<StateRef, SecureHash, CommittedState, PersistentStateRef>,
|
||||
services: ServiceHubInternal,
|
||||
notaryIdentityKey: PublicKey) : BFTSMaRt.Replica(config, replicaId, createMap, services, notaryIdentityKey) {
|
||||
|
||||
override fun executeCommand(command: ByteArray): ByteArray {
|
||||
val commitRequest = command.deserialize<BFTSMaRt.CommitRequest>()
|
||||
verifyRequest(commitRequest)
|
||||
val response = verifyAndCommitTx(commitRequest.payload.coreTransaction, commitRequest.callerIdentity, commitRequest.payload.requestSignature)
|
||||
return response.serialize().bytes
|
||||
}
|
||||
|
||||
private fun verifyAndCommitTx(transaction: CoreTransaction, callerIdentity: Party, requestSignature: NotarisationRequestSignature): BFTSMaRt.ReplicaResponse {
|
||||
return try {
|
||||
val id = transaction.id
|
||||
val inputs = transaction.inputs
|
||||
val references = transaction.references
|
||||
val notary = transaction.notary
|
||||
val timeWindow = (transaction as? FilteredTransaction)?.timeWindow
|
||||
if (notary !in services.myInfo.legalIdentities) throw NotaryInternalException(NotaryError.WrongNotary)
|
||||
commitInputStates(inputs, id, callerIdentity.name, requestSignature, timeWindow, references)
|
||||
log.debug { "Inputs committed successfully, signing $id" }
|
||||
BFTSMaRt.ReplicaResponse.Signature(sign(id))
|
||||
} catch (e: NotaryInternalException) {
|
||||
log.debug { "Error processing transaction: ${e.error}" }
|
||||
val serializedError = e.error.serialize()
|
||||
val errorSignature = sign(serializedError.bytes)
|
||||
val signedError = SignedData(serializedError, errorSignature)
|
||||
BFTSMaRt.ReplicaResponse.Error(signedError)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyRequest(commitRequest: BFTSMaRt.CommitRequest) {
|
||||
val transaction = commitRequest.payload.coreTransaction
|
||||
val notarisationRequest = NotarisationRequest(transaction.inputs, transaction.id)
|
||||
notarisationRequest.verifySignature(commitRequest.payload.requestSignature, commitRequest.callerIdentity)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
replicaHolder.getOrThrow().dispose()
|
||||
client.dispose()
|
||||
}
|
||||
}
|
@ -1,318 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import bftsmart.communication.ServerCommunicationSystem
|
||||
import bftsmart.communication.client.netty.NettyClientServerCommunicationSystemClientSide
|
||||
import bftsmart.communication.client.netty.NettyClientServerSession
|
||||
import bftsmart.statemanagement.strategy.StandardStateManager
|
||||
import bftsmart.tom.MessageContext
|
||||
import bftsmart.tom.ServiceProxy
|
||||
import bftsmart.tom.ServiceReplica
|
||||
import bftsmart.tom.core.TOMLayer
|
||||
import bftsmart.tom.core.messages.TOMMessage
|
||||
import bftsmart.tom.server.defaultservices.DefaultRecoverable
|
||||
import bftsmart.tom.server.defaultservices.DefaultReplier
|
||||
import bftsmart.tom.util.Extractor
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.NotarisationPayload
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.CordaX500Name
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.declaredField
|
||||
import net.corda.core.internal.notary.NotaryInternalException
|
||||
import net.corda.core.internal.notary.isConsumedByTheSameTx
|
||||
import net.corda.core.internal.notary.validateTimeWindow
|
||||
import net.corda.core.internal.toTypedArray
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.transactions.BFTSMaRt.Client
|
||||
import net.corda.node.services.transactions.BFTSMaRt.Replica
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import java.nio.file.Path
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Implements a replicated transaction commit log based on the [BFT-SMaRt](https://github.com/bft-smart/library)
|
||||
* consensus algorithm. Every replica in the cluster is running a [Replica] maintaining the state, and a [Client] is used
|
||||
* to relay state modification requests to all [Replica]s.
|
||||
*/
|
||||
// TODO: Define and document the configuration of the bft-smart cluster.
|
||||
// TODO: Potentially update the bft-smart API for our use case or rebuild client and server from lower level building
|
||||
// blocks bft-smart provides.
|
||||
// TODO: Support cluster membership changes. This requires reading about reconfiguration of bft-smart clusters and
|
||||
// perhaps a design doc. In general, it seems possible to use the state machine to reconfigure the cluster (reaching
|
||||
// consensus about membership changes). Nodes that join the cluster for the first time or re-join can go through
|
||||
// a "recovering" state and request missing data from their peers.
|
||||
object BFTSMaRt {
|
||||
/** Sent from [Client] to [Replica]. */
|
||||
@CordaSerializable
|
||||
data class CommitRequest(val payload: NotarisationPayload, val callerIdentity: Party)
|
||||
|
||||
/** Sent from [Replica] to [Client]. */
|
||||
@CordaSerializable
|
||||
sealed class ReplicaResponse {
|
||||
data class Error(val error: SignedData<NotaryError>) : ReplicaResponse()
|
||||
data class Signature(val txSignature: TransactionSignature) : ReplicaResponse()
|
||||
}
|
||||
|
||||
/** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */
|
||||
@CordaSerializable
|
||||
sealed class ClusterResponse {
|
||||
data class Error(val errors: List<SignedData<NotaryError>>) : ClusterResponse()
|
||||
data class Signatures(val txSignatures: List<TransactionSignature>) : ClusterResponse()
|
||||
}
|
||||
|
||||
interface Cluster {
|
||||
/** Avoid bug where a replica fails to start due to a consensus change during the BFT startup sequence. */
|
||||
fun waitUntilAllReplicasHaveInitialized()
|
||||
}
|
||||
|
||||
class Client(config: BFTSMaRtConfig, private val clientId: Int, private val cluster: Cluster, private val notaryService: BFTNonValidatingNotaryService) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
/** A proxy for communicating with the BFT cluster */
|
||||
private val proxy = ServiceProxy(clientId, config.path.toString(), buildResponseComparator(), buildExtractor())
|
||||
private val sessionTable = (proxy.communicationSystem as NettyClientServerCommunicationSystemClientSide).declaredField<Map<Int, NettyClientServerSession>>("sessionTable").value
|
||||
|
||||
fun dispose() {
|
||||
proxy.close() // XXX: Does this do enough?
|
||||
}
|
||||
|
||||
private fun awaitClientConnectionToCluster() {
|
||||
// TODO: Hopefully we only need to wait for the client's initial connection to the cluster, and this method can be moved to some startup code.
|
||||
// TODO: Investigate ConcurrentModificationException in this method.
|
||||
while (true) {
|
||||
val inactive = sessionTable.entries.mapNotNull { if (it.value.channel.isActive) null else it.key }
|
||||
if (inactive.isEmpty()) break
|
||||
log.info("Client-replica channels not yet active: $clientId to $inactive")
|
||||
Thread.sleep((inactive.size * 100).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a transaction commit request to the BFT cluster. The [proxy] will deliver the request to every
|
||||
* replica, and block until a sufficient number of replies are received.
|
||||
*/
|
||||
fun commitTransaction(payload: NotarisationPayload, otherSide: Party): ClusterResponse {
|
||||
awaitClientConnectionToCluster()
|
||||
cluster.waitUntilAllReplicasHaveInitialized()
|
||||
val requestBytes = CommitRequest(payload, otherSide).serialize().bytes
|
||||
val responseBytes = proxy.invokeOrdered(requestBytes)
|
||||
return responseBytes.deserialize()
|
||||
}
|
||||
|
||||
/** A comparator to check if replies from two replicas are the same. */
|
||||
private fun buildResponseComparator(): Comparator<ByteArray> {
|
||||
return Comparator { o1, o2 ->
|
||||
val reply1 = o1.deserialize<ReplicaResponse>()
|
||||
val reply2 = o2.deserialize<ReplicaResponse>()
|
||||
if (reply1 is ReplicaResponse.Error && reply2 is ReplicaResponse.Error) {
|
||||
// TODO: for now we treat all errors as equal, compare by error type as well
|
||||
0
|
||||
} else if (reply1 is ReplicaResponse.Signature && reply2 is ReplicaResponse.Signature) 0 else -1
|
||||
}
|
||||
}
|
||||
|
||||
/** An extractor to build the final response message for the client application from all received replica replies. */
|
||||
private fun buildExtractor(): Extractor {
|
||||
return Extractor { replies, _, lastReceived ->
|
||||
val responses = replies.mapNotNull { it?.content?.deserialize<ReplicaResponse>() }
|
||||
val accepted = responses.filterIsInstance<ReplicaResponse.Signature>()
|
||||
val rejected = responses.filterIsInstance<ReplicaResponse.Error>()
|
||||
|
||||
log.debug { "BFT Client $clientId: number of replicas accepted the commit: ${accepted.size}, rejected: ${rejected.size}" }
|
||||
|
||||
// TODO: only return an aggregate if the majority of signatures are replies
|
||||
// TODO: return an error reported by the majority and not just the first one
|
||||
val aggregateResponse = if (accepted.isNotEmpty()) {
|
||||
log.debug { "Cluster response - signatures: ${accepted.map { it.txSignature }}" }
|
||||
ClusterResponse.Signatures(accepted.map { it.txSignature })
|
||||
} else {
|
||||
log.debug { "Cluster response - error: ${rejected.first().error}" }
|
||||
ClusterResponse.Error(rejected.map { it.error })
|
||||
}
|
||||
|
||||
val messageContent = aggregateResponse.serialize().bytes
|
||||
// TODO: is it safe use the last message for sender/session/sequence info
|
||||
val reply = replies[lastReceived]
|
||||
TOMMessage(reply.sender, reply.session, reply.sequence, messageContent, reply.viewID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ServiceReplica doesn't have any kind of shutdown method, so we add one in this subclass. */
|
||||
private class CordaServiceReplica(replicaId: Int, configHome: Path, owner: DefaultRecoverable) : ServiceReplica(replicaId, configHome.toString(), owner, owner, null, DefaultReplier()) {
|
||||
private val tomLayerField = declaredField<TOMLayer>(ServiceReplica::class, "tomLayer")
|
||||
private val csField = declaredField<ServerCommunicationSystem>(ServiceReplica::class, "cs")
|
||||
fun dispose() {
|
||||
// Half of what restart does:
|
||||
val tomLayer = tomLayerField.value
|
||||
tomLayer.shutdown() // Non-blocking.
|
||||
val cs = csField.value
|
||||
cs.join()
|
||||
cs.serversConn.join()
|
||||
tomLayer.join()
|
||||
tomLayer.deliveryThread.join()
|
||||
// TODO: At the cluster level, join all Sender/Receiver threads.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maintains the commit log and executes commit commands received from the [Client].
|
||||
*
|
||||
* The validation logic can be specified by implementing the [executeCommand] method.
|
||||
*/
|
||||
abstract class Replica(config: BFTSMaRtConfig,
|
||||
replicaId: Int,
|
||||
createMap: () -> AppendOnlyPersistentMap<StateRef, SecureHash,
|
||||
BFTNonValidatingNotaryService.CommittedState, PersistentStateRef>,
|
||||
protected val services: ServiceHubInternal,
|
||||
protected val notaryIdentityKey: PublicKey) : DefaultRecoverable() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
}
|
||||
|
||||
private val stateManagerOverride = run {
|
||||
// Mock framework shutdown is not in reverse order, and we need to stop the faulty replicas first:
|
||||
val exposeStartupRace = config.exposeRaces && replicaId < maxFaultyReplicas(config.clusterSize)
|
||||
object : StandardStateManager() {
|
||||
override fun askCurrentConsensusId() {
|
||||
if (exposeStartupRace) Thread.sleep(20000) // Must be long enough for the non-redundant replicas to reach a non-initial consensus.
|
||||
super.askCurrentConsensusId()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getStateManager() = stateManagerOverride
|
||||
// Must be initialised before ServiceReplica is started
|
||||
private val commitLog = services.database.transaction { createMap() }
|
||||
private val replica = run {
|
||||
config.waitUntilReplicaWillNotPrintStackTrace(replicaId)
|
||||
@Suppress("LeakingThis")
|
||||
CordaServiceReplica(replicaId, config.path, this)
|
||||
}
|
||||
|
||||
fun dispose() {
|
||||
replica.dispose()
|
||||
}
|
||||
|
||||
override fun appExecuteUnordered(command: ByteArray, msgCtx: MessageContext): ByteArray? {
|
||||
throw NotImplementedError("No unordered operations supported")
|
||||
}
|
||||
|
||||
override fun appExecuteBatch(command: Array<ByteArray>, mcs: Array<MessageContext>): Array<ByteArray?> {
|
||||
return Arrays.stream(command).map(this::executeCommand).toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement logic to execute the command and commit the transaction to the log.
|
||||
* Helper methods are provided for transaction processing: [commitInputStates], and [sign].
|
||||
*/
|
||||
abstract fun executeCommand(command: ByteArray): ByteArray?
|
||||
|
||||
private fun checkConflict(
|
||||
conflictingStates: LinkedHashMap<StateRef, StateConsumptionDetails>,
|
||||
states: List<StateRef>,
|
||||
type: StateConsumptionDetails.ConsumedStateType
|
||||
) {
|
||||
states.forEach { stateRef ->
|
||||
commitLog[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.sha256(), type) }
|
||||
}
|
||||
}
|
||||
|
||||
protected fun commitInputStates(
|
||||
states: List<StateRef>,
|
||||
txId: SecureHash,
|
||||
callerName: CordaX500Name,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef> = emptyList()
|
||||
) {
|
||||
log.debug { "Attempting to commit inputs for transaction: $txId" }
|
||||
services.database.transaction {
|
||||
logRequest(txId, callerName, requestSignature)
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
|
||||
checkConflict(conflictingStates, states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
|
||||
checkConflict(conflictingStates, references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
if (!isConsumedByTheSameTx(txId.sha256(), conflictingStates)) {
|
||||
log.debug { "Failure, input states or references already committed: ${conflictingStates.keys}" }
|
||||
throw NotaryInternalException(NotaryError.Conflict(txId, conflictingStates))
|
||||
}
|
||||
} else {
|
||||
val outsideTimeWindowError = validateTimeWindow(services.clock.instant(), timeWindow)
|
||||
if (outsideTimeWindowError == null) {
|
||||
states.forEach { commitLog[it] = txId }
|
||||
log.debug { "Successfully committed all input states: $states" }
|
||||
} else {
|
||||
throw NotaryInternalException(outsideTimeWindowError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequest(txId: SecureHash, callerName: CordaX500Name, requestSignature: NotarisationRequestSignature) {
|
||||
val request = PersistentUniquenessProvider.Request(
|
||||
consumingTxHash = txId.toString(),
|
||||
partyName = callerName.toString(),
|
||||
requestSignature = requestSignature.serialize().bytes,
|
||||
requestDate = services.clock.instant()
|
||||
)
|
||||
val session = currentDBSession()
|
||||
session.persist(request)
|
||||
}
|
||||
|
||||
/** Generates a signature over an arbitrary array of bytes. */
|
||||
protected fun sign(bytes: ByteArray): DigitalSignature.WithKey {
|
||||
return services.database.transaction { services.keyManagementService.sign(bytes, notaryIdentityKey) }
|
||||
}
|
||||
|
||||
/** Generates a transaction signature over the specified transaction [txId]. */
|
||||
protected fun sign(txId: SecureHash): TransactionSignature {
|
||||
val signableData = SignableData(txId, SignatureMetadata(services.myInfo.platformVersion, Crypto.findSignatureScheme(notaryIdentityKey).schemeNumberID))
|
||||
return services.database.transaction { services.keyManagementService.sign(signableData, notaryIdentityKey) }
|
||||
}
|
||||
|
||||
// TODO:
|
||||
// - Test snapshot functionality with different bft-smart cluster configurations.
|
||||
// - Add streaming to support large data sets.
|
||||
override fun getSnapshot(): ByteArray {
|
||||
// LinkedHashMap for deterministic serialisation
|
||||
val committedStates = LinkedHashMap<StateRef, SecureHash>()
|
||||
val requests = services.database.transaction {
|
||||
commitLog.allPersisted().forEach { committedStates[it.first] = it.second }
|
||||
val criteriaQuery = session.criteriaBuilder.createQuery(PersistentUniquenessProvider.Request::class.java)
|
||||
criteriaQuery.select(criteriaQuery.from(PersistentUniquenessProvider.Request::class.java))
|
||||
session.createQuery(criteriaQuery).resultList
|
||||
}
|
||||
return (committedStates to requests).serialize().bytes
|
||||
}
|
||||
|
||||
override fun installSnapshot(bytes: ByteArray) {
|
||||
val (committedStates, requests) = bytes.deserialize<Pair<LinkedHashMap<StateRef, SecureHash>, List<PersistentUniquenessProvider.Request>>>()
|
||||
services.database.transaction {
|
||||
commitLog.clear()
|
||||
commitLog.putAll(committedStates)
|
||||
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentUniquenessProvider.Request::class.java)
|
||||
deleteQuery.from(PersistentUniquenessProvider.Request::class.java)
|
||||
session.createQuery(deleteQuery).executeUpdate()
|
||||
requests.forEach { session.persist(it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.core.internal.writer
|
||||
import net.corda.core.utilities.NetworkHostAndPort
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import java.io.PrintWriter
|
||||
import java.net.InetAddress
|
||||
import java.net.Socket
|
||||
import java.net.SocketException
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
|
||||
/**
|
||||
* BFT SMaRt can only be configured via files in a configHome directory.
|
||||
* Each instance of this class creates such a configHome, accessible via [path].
|
||||
* The files are deleted on [close] typically via [use], see [PathManager] for details.
|
||||
*/
|
||||
class BFTSMaRtConfig(private val replicaAddresses: List<NetworkHostAndPort>, debug: Boolean, val exposeRaces: Boolean) : PathManager<BFTSMaRtConfig>(Files.createTempDirectory("bft-smart-config")) {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
internal const val portIsClaimedFormat = "Port %s is claimed by another replica: %s"
|
||||
}
|
||||
|
||||
val clusterSize get() = replicaAddresses.size
|
||||
|
||||
init {
|
||||
val claimedPorts = mutableSetOf<NetworkHostAndPort>()
|
||||
val n = clusterSize
|
||||
(0 until n).forEach { replicaId ->
|
||||
// Each replica claims the configured port and the next one:
|
||||
replicaPorts(replicaId).forEach { port ->
|
||||
claimedPorts.add(port) || throw IllegalArgumentException(portIsClaimedFormat.format(port, claimedPorts))
|
||||
}
|
||||
}
|
||||
configWriter("hosts.config") {
|
||||
replicaAddresses.forEachIndexed { index, (host, port) ->
|
||||
// The documentation strongly recommends IP addresses:
|
||||
println("$index ${InetAddress.getByName(host).hostAddress} $port")
|
||||
}
|
||||
}
|
||||
val systemConfig = String.format(javaClass.getResource("system.config.printf").readText(), n, maxFaultyReplicas(n), if (debug) 1 else 0, (0 until n).joinToString(","))
|
||||
configWriter("system.config") {
|
||||
print(systemConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun configWriter(name: String, block: PrintWriter.() -> Unit) {
|
||||
// Default charset, consistent with loaders:
|
||||
(path / name).writer().use {
|
||||
PrintWriter(it).use {
|
||||
it.run(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun waitUntilReplicaWillNotPrintStackTrace(contextReplicaId: Int) {
|
||||
// A replica will printStackTrace until all lower-numbered replicas are listening.
|
||||
// But we can't probe a replica without it logging EOFException when our probe succeeds.
|
||||
// So to keep logging to a minimum we only check the previous replica:
|
||||
val peerId = contextReplicaId - 1
|
||||
if (peerId < 0) return
|
||||
// The printStackTrace we want to avoid is in replica-replica communication code:
|
||||
val address = BFTSMaRtPort.FOR_REPLICAS.ofReplica(replicaAddresses[peerId])
|
||||
log.debug { "Waiting for replica $peerId to start listening on: $address" }
|
||||
while (!address.isListening()) MILLISECONDS.sleep(200)
|
||||
log.debug { "Replica $peerId is ready for P2P." }
|
||||
}
|
||||
|
||||
private fun replicaPorts(replicaId: Int): List<NetworkHostAndPort> {
|
||||
val base = replicaAddresses[replicaId]
|
||||
return BFTSMaRtPort.values().map { it.ofReplica(base) }
|
||||
}
|
||||
}
|
||||
|
||||
private enum class BFTSMaRtPort(private val off: Int) {
|
||||
FOR_CLIENTS(0),
|
||||
FOR_REPLICAS(1);
|
||||
|
||||
fun ofReplica(base: NetworkHostAndPort) = NetworkHostAndPort(base.host, base.port + off)
|
||||
}
|
||||
|
||||
private fun NetworkHostAndPort.isListening() = try {
|
||||
Socket(host, port).use { true } // Will cause one error to be logged in the replica on success.
|
||||
} catch (e: SocketException) {
|
||||
false
|
||||
}
|
||||
|
||||
fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3
|
||||
fun minCorrectReplicas(clusterSize: Int) = (2 * clusterSize + 3) / 3
|
||||
fun minClusterSize(maxFaultyReplicas: Int) = maxFaultyReplicas * 3 + 1
|
||||
|
||||
fun bftSMaRtSerialFilter(clazz: Class<*>): Boolean = clazz.name.let {
|
||||
it.startsWith("bftsmart.")
|
||||
|| it.startsWith("java.security.")
|
||||
|| it.startsWith("java.util.")
|
||||
|| it.startsWith("java.lang.")
|
||||
|| it.startsWith("java.net.")
|
||||
}
|
@ -9,6 +9,7 @@ import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.internal.concurrent.OpenFuture
|
||||
import net.corda.core.internal.concurrent.openFuture
|
||||
import net.corda.core.internal.notary.AsyncUniquenessProvider
|
||||
@ -22,7 +23,6 @@ import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
|
@ -1,26 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||
import net.corda.core.internal.notary.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.ServiceHub
|
||||
import java.security.PublicKey
|
||||
|
||||
/** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */
|
||||
class RaftNonValidatingNotaryService(
|
||||
override val services: ServiceHub,
|
||||
override val notaryIdentityKey: PublicKey,
|
||||
override val uniquenessProvider: RaftUniquenessProvider
|
||||
) : TrustedAuthorityNotaryService() {
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow {
|
||||
return NonValidatingNotaryFlow(otherPartySession, this)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
uniquenessProvider.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
uniquenessProvider.stop()
|
||||
}
|
||||
}
|
@ -1,220 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import io.atomix.catalyst.buffer.BufferInput
|
||||
import io.atomix.catalyst.buffer.BufferOutput
|
||||
import io.atomix.catalyst.serializer.Serializer
|
||||
import io.atomix.catalyst.serializer.TypeSerializer
|
||||
import io.atomix.copycat.Command
|
||||
import io.atomix.copycat.Query
|
||||
import io.atomix.copycat.server.Commit
|
||||
import io.atomix.copycat.server.Snapshottable
|
||||
import io.atomix.copycat.server.StateMachine
|
||||
import io.atomix.copycat.server.storage.snapshot.SnapshotReader
|
||||
import io.atomix.copycat.server.storage.snapshot.SnapshotWriter
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.NotaryError
|
||||
import net.corda.core.flows.StateConsumptionDetails
|
||||
import net.corda.core.internal.VisibleForTesting
|
||||
import net.corda.core.internal.notary.isConsumedByTheSameTx
|
||||
import net.corda.core.internal.notary.validateTimeWindow
|
||||
import net.corda.core.serialization.*
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationDefaults
|
||||
import net.corda.core.serialization.internal.CheckpointSerializationFactory
|
||||
import net.corda.core.serialization.internal.checkpointSerialize
|
||||
import net.corda.core.utilities.ByteSequence
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import net.corda.serialization.internal.CordaSerializationEncoding
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* Notarised contract state commit log, replicated across a Copycat Raft cluster.
|
||||
*
|
||||
* Copycat ony supports in-memory state machines, so we back the state with JDBC tables.
|
||||
* State re-synchronisation is achieved by replaying the command log to the new (or re-joining) cluster member.
|
||||
*/
|
||||
class RaftTransactionCommitLog<E, EK>(
|
||||
private val db: CordaPersistence,
|
||||
private val nodeClock: Clock,
|
||||
createMap: () -> AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, E, EK>
|
||||
) : StateMachine(), Snapshottable {
|
||||
object Commands {
|
||||
class CommitTransaction @JvmOverloads constructor(
|
||||
val states: List<StateRef>,
|
||||
val txId: SecureHash,
|
||||
val requestingParty: String,
|
||||
val requestSignature: ByteArray,
|
||||
val timeWindow: TimeWindow? = null,
|
||||
val references: List<StateRef> = emptyList()
|
||||
) : Command<NotaryError?> {
|
||||
override fun compaction(): Command.CompactionMode {
|
||||
// The FULL compaction mode retains the command in the log until it has been stored and applied on all
|
||||
// servers in the cluster. Once the commit has been applied to a state machine and closed it may be
|
||||
// removed from the log during minor or major compaction.
|
||||
//
|
||||
// Note that we are not closing the commits, thus our log grows without bounds. We let the log grow on
|
||||
// purpose to be able to increase the size of a running cluster, e.g. to add and decommission nodes.
|
||||
// TODO: Cluster membership changes need testing.
|
||||
// TODO: I'm wondering if we should support resizing notary clusters, or if we could require users to
|
||||
// setup a new cluster of the desired size and transfer the data.
|
||||
return Command.CompactionMode.FULL
|
||||
}
|
||||
}
|
||||
|
||||
class Get(val key: StateRef) : Query<SecureHash?>
|
||||
}
|
||||
|
||||
private val map = db.transaction { createMap() }
|
||||
|
||||
/** Commits the input states for the transaction as specified in the given [Commands.CommitTransaction]. */
|
||||
fun commitTransaction(raftCommit: Commit<Commands.CommitTransaction>): NotaryError? {
|
||||
val conflictingStates = LinkedHashMap<StateRef, StateConsumptionDetails>()
|
||||
|
||||
fun checkConflict(states: List<StateRef>, type: StateConsumptionDetails.ConsumedStateType) = states.forEach { stateRef ->
|
||||
map[stateRef]?.let { conflictingStates[stateRef] = StateConsumptionDetails(it.second.sha256(), type) }
|
||||
}
|
||||
|
||||
raftCommit.use {
|
||||
val index = it.index()
|
||||
return db.transaction {
|
||||
val commitCommand = raftCommit.command()
|
||||
logRequest(commitCommand)
|
||||
val txId = commitCommand.txId
|
||||
log.debug("State machine commit: attempting to store entries with keys (${commitCommand.states.joinToString()})")
|
||||
checkConflict(commitCommand.states, StateConsumptionDetails.ConsumedStateType.INPUT_STATE)
|
||||
checkConflict(commitCommand.references, StateConsumptionDetails.ConsumedStateType.REFERENCE_INPUT_STATE)
|
||||
if (conflictingStates.isNotEmpty()) {
|
||||
if (isConsumedByTheSameTx(commitCommand.txId.sha256(), conflictingStates)) {
|
||||
null
|
||||
} else {
|
||||
log.debug { "Failure, input states already committed: ${conflictingStates.keys}" }
|
||||
NotaryError.Conflict(txId, conflictingStates)
|
||||
}
|
||||
} else {
|
||||
val outsideTimeWindowError = validateTimeWindow(clock.instant(), commitCommand.timeWindow)
|
||||
if (outsideTimeWindowError == null) {
|
||||
val entries = commitCommand.states.map { it to Pair(index, txId) }.toMap()
|
||||
map.putAll(entries)
|
||||
log.debug { "Successfully committed all input states: ${commitCommand.states}" }
|
||||
null
|
||||
} else {
|
||||
outsideTimeWindowError
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logRequest(commitCommand: RaftTransactionCommitLog.Commands.CommitTransaction) {
|
||||
val request = PersistentUniquenessProvider.Request(
|
||||
consumingTxHash = commitCommand.txId.toString(),
|
||||
partyName = commitCommand.requestingParty,
|
||||
requestSignature = commitCommand.requestSignature,
|
||||
requestDate = nodeClock.instant()
|
||||
)
|
||||
val session = currentDBSession()
|
||||
session.persist(request)
|
||||
}
|
||||
|
||||
/** Gets the consuming transaction id for a given state reference. */
|
||||
fun get(commit: Commit<Commands.Get>): SecureHash? {
|
||||
commit.use {
|
||||
val key = it.operation().key
|
||||
return db.transaction { map[key]?.second }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes out all committed state and notarisation request entries to disk. Note that this operation does not
|
||||
* load all entries into memory, as the [SnapshotWriter] is using a disk-backed buffer internally, and iterating
|
||||
* map entries results in only a fixed number of recently accessed entries to ever be kept in memory.
|
||||
*/
|
||||
override fun snapshot(writer: SnapshotWriter) {
|
||||
db.transaction {
|
||||
writer.writeInt(map.size)
|
||||
map.allPersisted().forEach {
|
||||
val bytes = it.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
|
||||
writer.writeUnsignedShort(bytes.size)
|
||||
writer.writeObject(bytes)
|
||||
}
|
||||
|
||||
val criteriaQuery = session.criteriaBuilder.createQuery(PersistentUniquenessProvider.Request::class.java)
|
||||
criteriaQuery.select(criteriaQuery.from(PersistentUniquenessProvider.Request::class.java))
|
||||
val results = session.createQuery(criteriaQuery).resultList
|
||||
|
||||
writer.writeInt(results.size)
|
||||
results.forEach {
|
||||
val bytes = it.serialize(context = SerializationDefaults.STORAGE_CONTEXT).bytes
|
||||
writer.writeUnsignedShort(bytes.size)
|
||||
writer.writeObject(bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads entries from disk and populates the committed state and notarisation request tables. */
|
||||
override fun install(reader: SnapshotReader) {
|
||||
val size = reader.readInt()
|
||||
db.transaction {
|
||||
map.clear()
|
||||
// TODO: read & put entries in batches
|
||||
for (i in 1..size) {
|
||||
val bytes = ByteArray(reader.readUnsignedShort())
|
||||
reader.read(bytes)
|
||||
val (key, value) = bytes.deserialize<Pair<StateRef, Pair<Long, SecureHash>>>()
|
||||
map[key] = value
|
||||
}
|
||||
// Clean notarisation request log
|
||||
val deleteQuery = session.criteriaBuilder.createCriteriaDelete(PersistentUniquenessProvider.Request::class.java)
|
||||
deleteQuery.from(PersistentUniquenessProvider.Request::class.java)
|
||||
session.createQuery(deleteQuery).executeUpdate()
|
||||
// Load and populate request log
|
||||
for (i in 1..reader.readInt()) {
|
||||
val bytes = ByteArray(reader.readUnsignedShort())
|
||||
reader.read(bytes)
|
||||
val request = bytes.deserialize<PersistentUniquenessProvider.Request>()
|
||||
session.persist(request)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
|
||||
@VisibleForTesting
|
||||
val serializer: Serializer by lazy {
|
||||
Serializer().apply {
|
||||
registerAbstract(SecureHash::class.java, CordaKryoSerializer::class.java)
|
||||
registerAbstract(TimeWindow::class.java, CordaKryoSerializer::class.java)
|
||||
registerAbstract(NotaryError::class.java, CordaKryoSerializer::class.java)
|
||||
register(RaftTransactionCommitLog.Commands.CommitTransaction::class.java, CordaKryoSerializer::class.java)
|
||||
register(RaftTransactionCommitLog.Commands.Get::class.java, CordaKryoSerializer::class.java)
|
||||
register(StateRef::class.java, CordaKryoSerializer::class.java)
|
||||
register(LinkedHashMap::class.java, CordaKryoSerializer::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
class CordaKryoSerializer<T : Any> : TypeSerializer<T> {
|
||||
private val context = CheckpointSerializationDefaults.CHECKPOINT_CONTEXT.withEncoding(CordaSerializationEncoding.SNAPPY)
|
||||
private val factory = CheckpointSerializationFactory.defaultFactory
|
||||
|
||||
override fun write(obj: T, buffer: BufferOutput<*>, serializer: Serializer) {
|
||||
val serialized = obj.checkpointSerialize(context = context)
|
||||
buffer.writeInt(serialized.size)
|
||||
buffer.write(serialized.bytes)
|
||||
}
|
||||
|
||||
override fun read(type: Class<T>, buffer: BufferInput<*>, serializer: Serializer): T {
|
||||
val size = buffer.readInt()
|
||||
val serialized = ByteArray(size)
|
||||
buffer.read(serialized)
|
||||
return factory.deserialize(ByteSequence.of(serialized), type, context)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,213 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import com.codahale.metrics.Gauge
|
||||
import com.codahale.metrics.MetricRegistry
|
||||
import io.atomix.catalyst.transport.Address
|
||||
import io.atomix.catalyst.transport.Transport
|
||||
import io.atomix.catalyst.transport.netty.NettyTransport
|
||||
import io.atomix.catalyst.transport.netty.SslProtocol
|
||||
import io.atomix.copycat.client.ConnectionStrategies
|
||||
import io.atomix.copycat.client.CopycatClient
|
||||
import io.atomix.copycat.client.RecoveryStrategies
|
||||
import io.atomix.copycat.server.CopycatServer
|
||||
import io.atomix.copycat.server.cluster.Member
|
||||
import io.atomix.copycat.server.storage.Storage
|
||||
import io.atomix.copycat.server.storage.StorageLevel
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TimeWindow
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.NotarisationRequestSignature
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.internal.notary.NotaryInternalException
|
||||
import net.corda.core.internal.notary.UniquenessProvider
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.node.services.config.RaftConfig
|
||||
import net.corda.node.services.transactions.RaftTransactionCommitLog.Commands.CommitTransaction
|
||||
import net.corda.node.utilities.AppendOnlyPersistentMap
|
||||
import net.corda.node.utilities.NamedCacheFactory
|
||||
import net.corda.nodeapi.internal.config.MutualSslConfiguration
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.NODE_DATABASE_PREFIX
|
||||
import java.nio.file.Path
|
||||
import java.time.Clock
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.EmbeddedId
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.Table
|
||||
|
||||
/**
|
||||
* A uniqueness provider that records committed input states in a distributed collection replicated and
|
||||
* persisted in a Raft cluster, using the Copycat framework (http://atomix.io/copycat/).
|
||||
*
|
||||
* The uniqueness provider maintains both a Copycat cluster node (server) and a client through which it can submit
|
||||
* requests to the cluster. In Copycat, a client request is first sent to the server it's connected to and then redirected
|
||||
* to the cluster leader to be actioned.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class RaftUniquenessProvider(
|
||||
private val storagePath: Path,
|
||||
private val transportConfiguration: MutualSslConfiguration,
|
||||
private val db: CordaPersistence,
|
||||
private val clock: Clock,
|
||||
private val metrics: MetricRegistry,
|
||||
private val cacheFactory: NamedCacheFactory,
|
||||
private val raftConfig: RaftConfig
|
||||
) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = contextLogger()
|
||||
fun createMap(cacheFactory: NamedCacheFactory): AppendOnlyPersistentMap<StateRef, Pair<Long, SecureHash>, CommittedState, PersistentStateRef> =
|
||||
AppendOnlyPersistentMap(
|
||||
cacheFactory = cacheFactory,
|
||||
name = "RaftUniquenessProvider_transactions",
|
||||
toPersistentEntityKey = { PersistentStateRef(it) },
|
||||
fromPersistentEntity = {
|
||||
val txId = it.id.txId
|
||||
val index = it.id.index
|
||||
Pair(
|
||||
StateRef(txhash = SecureHash.parse(txId), index = index),
|
||||
Pair(it.index, SecureHash.parse(it.value) as SecureHash))
|
||||
|
||||
},
|
||||
toPersistentEntity = { k: StateRef, (first, second) ->
|
||||
CommittedState(
|
||||
PersistentStateRef(k),
|
||||
second.toString(),
|
||||
first)
|
||||
|
||||
},
|
||||
persistentEntityClass = CommittedState::class.java
|
||||
)
|
||||
|
||||
fun StateRef.encoded() = "$txhash:$index"
|
||||
fun String.parseStateRef() = split(":").let { StateRef(SecureHash.parse(it[0]), it[1].toInt()) }
|
||||
}
|
||||
|
||||
@Entity
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}raft_committed_states")
|
||||
class CommittedState(
|
||||
@EmbeddedId
|
||||
val id: PersistentStateRef,
|
||||
@Column(name = "consuming_transaction_id", nullable = true)
|
||||
var value: String? = "",
|
||||
@Column(name = "raft_log_index", nullable = false)
|
||||
var index: Long = 0
|
||||
)
|
||||
|
||||
private lateinit var _clientFuture: CompletableFuture<CopycatClient>
|
||||
private lateinit var server: CopycatServer
|
||||
|
||||
/**
|
||||
* Copycat clients are responsible for connecting to the cluster and submitting commands and queries that operate
|
||||
* on the cluster's replicated state machine.
|
||||
*/
|
||||
private val client: CopycatClient
|
||||
get() = _clientFuture.get()
|
||||
|
||||
fun start() {
|
||||
log.info("Creating Copycat server, log stored in: ${storagePath.toAbsolutePath()}")
|
||||
val stateMachineFactory = {
|
||||
RaftTransactionCommitLog(db, clock, { createMap(cacheFactory) })
|
||||
}
|
||||
val address = raftConfig.nodeAddress.let { Address(it.host, it.port) }
|
||||
val storage = buildStorage(storagePath)
|
||||
val transport = buildTransport(transportConfiguration)
|
||||
|
||||
server = CopycatServer.builder(address)
|
||||
.withStateMachine(stateMachineFactory)
|
||||
.withStorage(storage)
|
||||
.withServerTransport(transport)
|
||||
.withSerializer(RaftTransactionCommitLog.serializer)
|
||||
.build()
|
||||
|
||||
val serverFuture = if (raftConfig.clusterAddresses.isNotEmpty()) {
|
||||
log.info("Joining an existing Copycat cluster at ${raftConfig.clusterAddresses}")
|
||||
val cluster = raftConfig.clusterAddresses.map { Address(it.host, it.port) }
|
||||
server.join(cluster)
|
||||
} else {
|
||||
log.info("Bootstrapping a Copycat cluster at $address")
|
||||
server.bootstrap()
|
||||
}
|
||||
|
||||
registerMonitoring()
|
||||
|
||||
val client = CopycatClient.builder(address)
|
||||
.withTransport(transport) // TODO: use local transport for client-server communications
|
||||
.withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF)
|
||||
.withSerializer(RaftTransactionCommitLog.serializer)
|
||||
.withRecoveryStrategy(RecoveryStrategies.RECOVER)
|
||||
.build()
|
||||
_clientFuture = serverFuture.thenCompose { client.connect(address) }
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
private fun buildStorage(storagePath: Path): Storage? {
|
||||
return Storage.builder()
|
||||
.withDirectory(storagePath.toFile())
|
||||
.withStorageLevel(StorageLevel.DISK)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildTransport(config: MutualSslConfiguration): Transport? {
|
||||
return NettyTransport.builder()
|
||||
.withSsl()
|
||||
.withSslProtocol(SslProtocol.TLSv1_2)
|
||||
.withKeyStorePath(config.keyStore.path.toString())
|
||||
.withKeyStorePassword(config.keyStore.password)
|
||||
.withTrustStorePath(config.trustStore.path.toString())
|
||||
.withTrustStorePassword(config.trustStore.password)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun registerMonitoring() {
|
||||
metrics.register("RaftCluster.ThisServerStatus", Gauge<String> {
|
||||
server.state().name
|
||||
})
|
||||
metrics.register("RaftCluster.MembersCount", Gauge<Int> {
|
||||
server.cluster().members().size
|
||||
})
|
||||
metrics.register("RaftCluster.Members", Gauge<List<String>> {
|
||||
server.cluster().members().map { it.address().toString() }
|
||||
})
|
||||
|
||||
metrics.register("RaftCluster.AvailableMembers", Gauge<List<String>> {
|
||||
server.cluster().members().filter { it.status() == Member.Status.AVAILABLE }.map { it.address().toString() }
|
||||
})
|
||||
|
||||
metrics.register("RaftCluster.AvailableMembersCount", Gauge<Int> {
|
||||
server.cluster().members().filter { it.status() == Member.Status.AVAILABLE }.size
|
||||
})
|
||||
}
|
||||
|
||||
override fun commit(
|
||||
states: List<StateRef>,
|
||||
txId: SecureHash,
|
||||
callerIdentity: Party,
|
||||
requestSignature: NotarisationRequestSignature,
|
||||
timeWindow: TimeWindow?,
|
||||
references: List<StateRef>
|
||||
) {
|
||||
log.debug { "Attempting to commit input states: ${states.joinToString()}" }
|
||||
val commitCommand = CommitTransaction(
|
||||
states,
|
||||
txId,
|
||||
callerIdentity.name.toString(),
|
||||
requestSignature.serialize().bytes,
|
||||
timeWindow,
|
||||
references
|
||||
)
|
||||
val commitError = client.submit(commitCommand).get()
|
||||
if (commitError != null) throw NotaryInternalException(commitError)
|
||||
log.debug { "All input states of transaction $txId have been committed" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,26 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||
import net.corda.core.internal.notary.TrustedAuthorityNotaryService
|
||||
import net.corda.core.node.ServiceHub
|
||||
import java.security.PublicKey
|
||||
|
||||
/** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */
|
||||
class RaftValidatingNotaryService(
|
||||
override val services: ServiceHub,
|
||||
override val notaryIdentityKey: PublicKey,
|
||||
override val uniquenessProvider: RaftUniquenessProvider
|
||||
) : TrustedAuthorityNotaryService() {
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow {
|
||||
return ValidatingNotaryFlow(otherPartySession, this)
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
uniquenessProvider.start()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
uniquenessProvider.stop()
|
||||
}
|
||||
}
|
@ -3,15 +3,38 @@ package net.corda.node.services.transactions
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||
import net.corda.core.internal.notary.TrustedAuthorityNotaryService
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import java.security.PublicKey
|
||||
|
||||
/** A simple Notary service that does not perform transaction validation */
|
||||
/** An embedded notary service that uses the node's database to store committed states. */
|
||||
class SimpleNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
private val notaryConfig = services.configuration.notary
|
||||
?: throw IllegalArgumentException("Failed to register ${this::class.java}: notary configuration not present")
|
||||
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = NonValidatingNotaryFlow(otherPartySession, this)
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow {
|
||||
return if (notaryConfig.validating) {
|
||||
log.info("Starting in validating mode")
|
||||
ValidatingNotaryFlow(otherPartySession, this)
|
||||
} else {
|
||||
log.info("Starting in non-validating mode")
|
||||
NonValidatingNotaryFlow(otherPartySession, this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
}
|
||||
|
||||
// Entities used by a Notary
|
||||
object NodeNotarySchema
|
||||
|
||||
object NodeNotarySchemaV1 : MappedSchema(schemaFamily = NodeNotarySchema.javaClass, version = 1,
|
||||
mappedTypes = listOf(PersistentUniquenessProvider.BaseComittedState::class.java,
|
||||
PersistentUniquenessProvider.Request::class.java,
|
||||
PersistentUniquenessProvider.CommittedState::class.java
|
||||
)) {
|
||||
override val migrationResource = "node-notary.changelog-master"
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.internal.notary.NotaryServiceFlow
|
||||
import net.corda.core.internal.notary.TrustedAuthorityNotaryService
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import java.security.PublicKey
|
||||
|
||||
/** A Notary service that validates the transaction chain of the submitted transaction before committing it */
|
||||
class ValidatingNotaryService(override val services: ServiceHubInternal, override val notaryIdentityKey: PublicKey) : TrustedAuthorityNotaryService() {
|
||||
override val uniquenessProvider = PersistentUniquenessProvider(services.clock, services.database, services.cacheFactory)
|
||||
|
||||
override fun createServiceFlow(otherPartySession: FlowSession): NotaryServiceFlow = ValidatingNotaryFlow(otherPartySession, this)
|
||||
|
||||
override fun start() {}
|
||||
override fun stop() {}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.services.upgrade
|
||||
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.UpgradedContract
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.node.services.ContractUpgradeService
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.utilities.PersistentMap
|
||||
@ -11,7 +12,7 @@ import javax.persistence.Entity
|
||||
import javax.persistence.Id
|
||||
import javax.persistence.Table
|
||||
|
||||
class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsToken() {
|
||||
class ContractUpgradeServiceImpl(cacheFactory: NamedCacheFactory) : ContractUpgradeService, SingletonSerializeAsToken() {
|
||||
|
||||
@Entity
|
||||
@Table(name = "${NODE_DATABASE_PREFIX}contract_upgrades")
|
||||
@ -26,7 +27,7 @@ class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsT
|
||||
)
|
||||
|
||||
private companion object {
|
||||
fun createContractUpgradesMap(): PersistentMap<String, String, DBContractUpgrade, String> {
|
||||
fun createContractUpgradesMap(cacheFactory: NamedCacheFactory): PersistentMap<String, String, DBContractUpgrade, String> {
|
||||
return PersistentMap(
|
||||
"ContractUpgradeService_upgrades",
|
||||
toPersistentEntityKey = { it },
|
||||
@ -37,12 +38,13 @@ class ContractUpgradeServiceImpl : ContractUpgradeService, SingletonSerializeAsT
|
||||
upgradedContractClassName = value
|
||||
}
|
||||
},
|
||||
persistentEntityClass = DBContractUpgrade::class.java
|
||||
persistentEntityClass = DBContractUpgrade::class.java,
|
||||
cacheFactory = cacheFactory
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val authorisedUpgrade = createContractUpgradesMap()
|
||||
private val authorisedUpgrade = createContractUpgradesMap(cacheFactory)
|
||||
|
||||
fun start() {
|
||||
authorisedUpgrade.preload()
|
||||
|
@ -225,6 +225,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
private val rootEntities = mutableMapOf<Class<out PersistentState>, Root<*>>(Pair(VaultSchemaV1.VaultStates::class.java, vaultStates))
|
||||
private val aggregateExpressions = mutableListOf<Expression<*>>()
|
||||
private val commonPredicates = mutableMapOf<Pair<String, Operator>, Predicate>() // schema attribute Name, operator -> predicate
|
||||
private val constraintPredicates = mutableSetOf<Predicate>()
|
||||
|
||||
var stateTypes: Vault.StateStatus = Vault.StateStatus.UNCONSUMED
|
||||
|
||||
@ -508,7 +509,7 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
else
|
||||
aggregateExpressions
|
||||
criteriaQuery.multiselect(selections)
|
||||
val combinedPredicates = commonPredicates.values.plus(predicateSet)
|
||||
val combinedPredicates = commonPredicates.values.plus(predicateSet).plus(constraintPredicates)
|
||||
criteriaQuery.where(*combinedPredicates.toTypedArray())
|
||||
|
||||
return predicateSet
|
||||
@ -561,6 +562,38 @@ class HibernateQueryCriteriaParser(val contractStateType: Class<out ContractStat
|
||||
}
|
||||
}
|
||||
|
||||
// contract constraint types
|
||||
if (criteria.constraintTypes.isNotEmpty()) {
|
||||
val predicateID = Pair(VaultSchemaV1.VaultStates::constraintType.name, IN)
|
||||
if (commonPredicates.containsKey(predicateID)) {
|
||||
val existingTypes = (commonPredicates[predicateID]!!.expressions[0] as InPredicate<*>).values.map { (it as LiteralExpression).literal }.toSet()
|
||||
if (existingTypes != criteria.constraintTypes) {
|
||||
log.warn("Enriching previous attribute [${VaultSchemaV1.VaultStates::constraintType.name}] values [$existingTypes] with [${criteria.constraintTypes}]")
|
||||
commonPredicates.replace(predicateID, criteriaBuilder.and(vaultStates.get<Vault.ConstraintInfo.Type>(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes.plus(existingTypes))))
|
||||
}
|
||||
} else {
|
||||
commonPredicates[predicateID] = criteriaBuilder.and(vaultStates.get<Vault.ConstraintInfo.Type>(VaultSchemaV1.VaultStates::constraintType.name).`in`(criteria.constraintTypes))
|
||||
}
|
||||
}
|
||||
|
||||
// contract constraint information (type and data)
|
||||
if (criteria.constraints.isNotEmpty()) {
|
||||
criteria.constraints.forEach { constraint ->
|
||||
val predicateConstraintType = criteriaBuilder.equal(vaultStates.get<Vault.ConstraintInfo>(VaultSchemaV1.VaultStates::constraintType.name), constraint.type())
|
||||
if (constraint.data() != null) {
|
||||
val predicateConstraintData = criteriaBuilder.equal(vaultStates.get<Vault.ConstraintInfo>(VaultSchemaV1.VaultStates::constraintData.name), constraint.data())
|
||||
val compositePredicate = criteriaBuilder.and(predicateConstraintType, predicateConstraintData)
|
||||
if (constraintPredicates.isNotEmpty()) {
|
||||
val previousPredicate = constraintPredicates.last()
|
||||
constraintPredicates.clear()
|
||||
constraintPredicates.add(criteriaBuilder.or(previousPredicate, compositePredicate))
|
||||
}
|
||||
else constraintPredicates.add(compositePredicate)
|
||||
}
|
||||
else constraintPredicates.add(criteriaBuilder.or(predicateConstraintType))
|
||||
}
|
||||
}
|
||||
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.messaging.DataFeed
|
||||
import net.corda.core.node.ServicesForResolution
|
||||
import net.corda.core.node.StatesToRecord
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.node.services.Vault.ConstraintInfo.Companion.constraintInfo
|
||||
import net.corda.core.node.services.vault.*
|
||||
import net.corda.core.schemas.PersistentStateRef
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
@ -132,12 +133,15 @@ class NodeVaultService(
|
||||
// Adding a new column in the "VaultStates" table was considered the best approach.
|
||||
val keys = stateOnly.participants.map { it.owningKey }
|
||||
val isRelevant = isRelevant(stateOnly, keyManagementService.filterMyKeys(keys).toSet())
|
||||
val constraintInfo = Vault.ConstraintInfo(stateAndRef.value.state.constraint)
|
||||
val stateToAdd = VaultSchemaV1.VaultStates(
|
||||
notary = stateAndRef.value.state.notary,
|
||||
contractStateClassName = stateAndRef.value.state.data.javaClass.name,
|
||||
stateStatus = Vault.StateStatus.UNCONSUMED,
|
||||
recordedTime = clock.instant(),
|
||||
relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT
|
||||
relevancyStatus = if (isRelevant) Vault.RelevancyStatus.RELEVANT else Vault.RelevancyStatus.NOT_RELEVANT,
|
||||
constraintType = constraintInfo.type(),
|
||||
constraintData = constraintInfo.data()
|
||||
)
|
||||
stateToAdd.stateRef = PersistentStateRef(stateAndRef.key)
|
||||
session.save(stateToAdd)
|
||||
@ -271,7 +275,10 @@ class NodeVaultService(
|
||||
val uuid = (Strand.currentStrand() as? FlowStateMachineImpl<*>)?.id?.uuid
|
||||
val vaultUpdate = if (uuid != null) netUpdate.copy(flowId = uuid) else netUpdate
|
||||
if (uuid != null) {
|
||||
val fungible = netUpdate.produced.filter { it.state.data is FungibleAsset<*> }
|
||||
val fungible = netUpdate.produced.filter { stateAndRef ->
|
||||
val state = stateAndRef.state.data
|
||||
state is FungibleAsset<*> || state is FungibleState<*>
|
||||
}
|
||||
if (fungible.isNotEmpty()) {
|
||||
val stateRefs = fungible.map { it.ref }.toNonEmptySet()
|
||||
log.trace { "Reserving soft locks for flow id $uuid and states $stateRefs" }
|
||||
@ -390,14 +397,27 @@ class NodeVaultService(
|
||||
|
||||
@Suspendable
|
||||
@Throws(StatesNotAvailableException::class)
|
||||
override fun <T : FungibleAsset<U>, U : Any> tryLockFungibleStatesForSpending(lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<U>,
|
||||
contractStateType: Class<out T>): List<StateAndRef<T>> {
|
||||
override fun <T : FungibleState<*>> tryLockFungibleStatesForSpending(
|
||||
lockId: UUID,
|
||||
eligibleStatesQuery: QueryCriteria,
|
||||
amount: Amount<*>,
|
||||
contractStateType: Class<out T>
|
||||
): List<StateAndRef<T>> {
|
||||
if (amount.quantity == 0L) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Helper to unwrap the token from the Issued object if one exists.
|
||||
fun unwrapIssuedAmount(amount: Amount<*>): Any {
|
||||
val token = amount.token
|
||||
return when (token) {
|
||||
is Issued<*> -> token.product
|
||||
else -> token
|
||||
}
|
||||
}
|
||||
|
||||
val unwrappedToken = unwrapIssuedAmount(amount)
|
||||
|
||||
// Enrich QueryCriteria with additional default attributes (such as soft locks).
|
||||
// We only want to return RELEVANT states here.
|
||||
val sortAttribute = SortAttribute.Standard(Sort.CommonStateAttribute.STATE_REF)
|
||||
@ -412,8 +432,10 @@ class NodeVaultService(
|
||||
var claimedAmount = 0L
|
||||
val claimedStates = mutableListOf<StateAndRef<T>>()
|
||||
for (state in results.states) {
|
||||
val issuedAssetToken = state.state.data.amount.token
|
||||
if (issuedAssetToken.product == amount.token) {
|
||||
// This method handles Amount<Issued<T>> in FungibleAsset and Amount<T> in FungibleState.
|
||||
val issuedAssetToken = unwrapIssuedAmount(state.state.data.amount)
|
||||
|
||||
if (issuedAssetToken == unwrappedToken) {
|
||||
claimedStates += state
|
||||
claimedAmount += state.state.data.amount.quantity
|
||||
if (claimedAmount > amount.quantity) {
|
||||
@ -514,7 +536,9 @@ class NodeVaultService(
|
||||
vaultState.notary,
|
||||
vaultState.lockId,
|
||||
vaultState.lockUpdateTime,
|
||||
vaultState.relevancyStatus))
|
||||
vaultState.relevancyStatus,
|
||||
constraintInfo(vaultState.constraintType, vaultState.constraintData)
|
||||
))
|
||||
} else {
|
||||
// TODO: improve typing of returned other results
|
||||
log.debug { "OtherResults: ${Arrays.toString(result.toArray())}" }
|
||||
|
@ -5,6 +5,7 @@ import net.corda.core.contracts.MAX_ISSUER_REF_SIZE
|
||||
import net.corda.core.contracts.UniqueIdentifier
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.MAX_CONSTRAINT_DATA_SIZE
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.schemas.MappedSchema
|
||||
import net.corda.core.schemas.PersistentState
|
||||
@ -66,7 +67,16 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
|
||||
/** refers to the last time a lock was taken (reserved) or updated (released, re-reserved) */
|
||||
@Column(name = "lock_timestamp", nullable = true)
|
||||
var lockUpdateTime: Instant? = null
|
||||
var lockUpdateTime: Instant? = null,
|
||||
|
||||
/** refers to constraint type (none, hash, whitelisted, signature) associated with a contract state */
|
||||
@Column(name = "constraint_type", nullable = false)
|
||||
var constraintType: Vault.ConstraintInfo.Type,
|
||||
|
||||
/** associated constraint type data (if any) */
|
||||
@Column(name = "constraint_data", length = MAX_CONSTRAINT_DATA_SIZE, nullable = true)
|
||||
@Type(type = "corda-wrapper-binary")
|
||||
var constraintData: ByteArray? = null
|
||||
) : PersistentState()
|
||||
|
||||
@Entity
|
||||
@ -135,9 +145,9 @@ object VaultSchemaV1 : MappedSchema(schemaFamily = VaultSchema.javaClass, versio
|
||||
@Column(name = "issuer_name", nullable = true)
|
||||
var issuer: AbstractParty?,
|
||||
|
||||
@Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = false)
|
||||
@Column(name = "issuer_ref", length = MAX_ISSUER_REF_SIZE, nullable = true)
|
||||
@Type(type = "corda-wrapper-binary")
|
||||
var issuerRef: ByteArray
|
||||
var issuerRef: ByteArray?
|
||||
) : PersistentState() {
|
||||
constructor(_owner: AbstractParty, _quantity: Long, _issuerParty: AbstractParty, _issuerRef: OpaqueBytes, _participants: List<AbstractParty>) :
|
||||
this(owner = _owner,
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.utilities
|
||||
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.github.benmanes.caffeine.cache.Weigher
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction
|
||||
import net.corda.nodeapi.internal.persistence.contextTransaction
|
||||
@ -12,7 +13,6 @@ import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicReference
|
||||
|
||||
|
||||
/**
|
||||
* Implements a caching layer on top of an *append-only* table accessed via Hibernate mapping. Note that if the same key is [set] twice,
|
||||
* typically this will result in a duplicate insert if this is racing with another transaction. The flow framework will then retry.
|
||||
|
@ -5,50 +5,77 @@ import com.github.benmanes.caffeine.cache.Cache
|
||||
import com.github.benmanes.caffeine.cache.CacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import net.corda.core.internal.buildNamed
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.serialization.SerializeAsToken
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Allow passing metrics and config to caching implementations.
|
||||
* Allow passing metrics and config to caching implementations. This is needs to be distinct from [NamedCacheFactory]
|
||||
* to avoid deterministic serialization from seeing metrics and config on method signatures.
|
||||
*/
|
||||
interface NamedCacheFactory : SerializeAsToken {
|
||||
interface BindableNamedCacheFactory : NamedCacheFactory, SerializeAsToken {
|
||||
/**
|
||||
* Build a new cache factory of the same type that incorporates metrics.
|
||||
*/
|
||||
fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory
|
||||
fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory
|
||||
|
||||
/**
|
||||
* Build a new cache factory of the same type that incorporates the associated configuration.
|
||||
*/
|
||||
fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory
|
||||
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V>
|
||||
fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V>
|
||||
fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory
|
||||
}
|
||||
|
||||
class DefaultNamedCacheFactory private constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : NamedCacheFactory, SingletonSerializeAsToken() {
|
||||
open class DefaultNamedCacheFactory protected constructor(private val metricRegistry: MetricRegistry?, private val nodeConfiguration: NodeConfiguration?) : BindableNamedCacheFactory, SingletonSerializeAsToken() {
|
||||
constructor() : this(null, null)
|
||||
|
||||
override fun bindWithMetrics(metricRegistry: MetricRegistry): NamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration)
|
||||
override fun bindWithConfig(nodeConfiguration: NodeConfiguration): NamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration)
|
||||
override fun bindWithMetrics(metricRegistry: MetricRegistry): BindableNamedCacheFactory = DefaultNamedCacheFactory(metricRegistry, this.nodeConfiguration)
|
||||
override fun bindWithConfig(nodeConfiguration: NodeConfiguration): BindableNamedCacheFactory = DefaultNamedCacheFactory(this.metricRegistry, nodeConfiguration)
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> {
|
||||
open protected fun <K, V> configuredForNamed(caffeine: Caffeine<K, V>, name: String): Caffeine<K, V> {
|
||||
return with(nodeConfiguration!!) {
|
||||
when {
|
||||
name.startsWith("RPCSecurityManagerShiroCache_") -> with(security?.authService?.options?.cache!!) { caffeine.maximumSize(maxEntries).expireAfterWrite(expireAfterSecs, TimeUnit.SECONDS) }
|
||||
name == "RPCServer_observableSubscription" -> caffeine
|
||||
name == "RpcClientProxyHandler_rpcObservable" -> caffeine
|
||||
name == "SerializationScheme_attachmentClassloader" -> caffeine
|
||||
name == "HibernateConfiguration_sessionFactories" -> caffeine.maximumSize(database.mappedSchemaCacheSize)
|
||||
name == "DBTransactionStorage_transactions" -> caffeine.maximumWeight(transactionCacheSizeBytes)
|
||||
name == "NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(attachmentContentCacheSizeBytes)
|
||||
name == "NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(attachmentCacheBound)
|
||||
name == "PersistentIdentityService_partyByKey" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "PersistentIdentityService_partyByName" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "PersistentNetworkMap_nodesByKey" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "PersistentNetworkMap_idByLegalName" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "PersistentKeyManagementService_keys" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "FlowDrainingMode_nodeProperties" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "ContractUpgradeService_upgrades" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "PersistentUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "P2PMessageDeduplicator_processedMessages" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "DeduplicationChecker_watermark" -> caffeine
|
||||
name == "BFTNonValidatingNotaryService_transactions" -> caffeine.maximumSize(defaultCacheSize)
|
||||
name == "RaftUniquenessProvider_transactions" -> caffeine.maximumSize(defaultCacheSize)
|
||||
else -> throw IllegalArgumentException("Unexpected cache name $name. Did you add a new cache?")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected fun checkState(name: String) {
|
||||
checkCacheName(name)
|
||||
checkNotNull(metricRegistry)
|
||||
checkNotNull(nodeConfiguration)
|
||||
return caffeine.maximumSize(1024).buildNamed<K, V>(name)
|
||||
}
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String): Cache<K, V> {
|
||||
checkState(name)
|
||||
return configuredForNamed(caffeine, name).build<K, V>()
|
||||
}
|
||||
|
||||
override fun <K, V> buildNamed(caffeine: Caffeine<in K, in V>, name: String, loader: CacheLoader<K, V>): LoadingCache<K, V> {
|
||||
checkNotNull(metricRegistry)
|
||||
checkNotNull(nodeConfiguration)
|
||||
val configuredCaffeine = when (name) {
|
||||
"DBTransactionStorage_transactions" -> caffeine.maximumWeight(nodeConfiguration!!.transactionCacheSizeBytes)
|
||||
"NodeAttachmentService_attachmentContent" -> caffeine.maximumWeight(nodeConfiguration!!.attachmentContentCacheSizeBytes)
|
||||
"NodeAttachmentService_attachmentPresence" -> caffeine.maximumSize(nodeConfiguration!!.attachmentCacheBound)
|
||||
else -> caffeine.maximumSize(1024)
|
||||
}
|
||||
return configuredCaffeine.buildNamed<K, V>(name, loader)
|
||||
checkState(name)
|
||||
return configuredForNamed(caffeine, name).build<K, V>(loader)
|
||||
}
|
||||
|
||||
open protected val defaultCacheSize = 1024L
|
||||
}
|
@ -4,6 +4,7 @@ import com.github.benmanes.caffeine.cache.CacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.github.benmanes.caffeine.cache.Weigher
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
|
||||
class NonInvalidatingCache<K, V> private constructor(
|
||||
val cache: LoadingCache<K, V>
|
||||
|
@ -5,21 +5,21 @@ import com.github.benmanes.caffeine.cache.CacheLoader
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import com.github.benmanes.caffeine.cache.LoadingCache
|
||||
import com.github.benmanes.caffeine.cache.RemovalListener
|
||||
import net.corda.core.internal.buildNamed
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
|
||||
class NonInvalidatingUnboundCache<K, V> private constructor(
|
||||
val cache: LoadingCache<K, V>
|
||||
) : LoadingCache<K, V> by cache {
|
||||
|
||||
constructor(name: String, loadFunction: (K) -> V, removalListener: RemovalListener<K, V> = RemovalListener { _, _, _ -> },
|
||||
constructor(name: String, cacheFactory: NamedCacheFactory, loadFunction: (K) -> V, removalListener: RemovalListener<K, V> = RemovalListener { _, _, _ -> },
|
||||
keysToPreload: () -> Iterable<K> = { emptyList() }) :
|
||||
this(buildCache(name, loadFunction, removalListener, keysToPreload))
|
||||
this(buildCache(name, cacheFactory, loadFunction, removalListener, keysToPreload))
|
||||
|
||||
private companion object {
|
||||
private fun <K, V> buildCache(name: String, loadFunction: (K) -> V, removalListener: RemovalListener<K, V>,
|
||||
private fun <K, V> buildCache(name: String, cacheFactory: NamedCacheFactory, loadFunction: (K) -> V, removalListener: RemovalListener<K, V>,
|
||||
keysToPreload: () -> Iterable<K>): LoadingCache<K, V> {
|
||||
val builder = Caffeine.newBuilder().removalListener(removalListener).executor(SameThreadExecutor.getExecutor())
|
||||
return builder.buildNamed(name, NonInvalidatingCacheLoader(loadFunction)).apply {
|
||||
return cacheFactory.buildNamed(builder, name, NonInvalidatingCacheLoader(loadFunction)).apply {
|
||||
getAll(keysToPreload())
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.node.utilities
|
||||
|
||||
import com.github.benmanes.caffeine.cache.RemovalCause
|
||||
import com.github.benmanes.caffeine.cache.RemovalListener
|
||||
import net.corda.core.internal.NamedCacheFactory
|
||||
import net.corda.core.utilities.contextLogger
|
||||
import net.corda.nodeapi.internal.persistence.currentDBSession
|
||||
import java.util.*
|
||||
@ -14,7 +15,8 @@ class PersistentMap<K : Any, V, E, out EK>(
|
||||
val toPersistentEntityKey: (K) -> EK,
|
||||
val fromPersistentEntity: (E) -> Pair<K, V>,
|
||||
val toPersistentEntity: (key: K, value: V) -> E,
|
||||
val persistentEntityClass: Class<E>
|
||||
val persistentEntityClass: Class<E>,
|
||||
cacheFactory: NamedCacheFactory
|
||||
) : MutableMap<K, V>, AbstractMap<K, V>() {
|
||||
|
||||
private companion object {
|
||||
@ -24,7 +26,8 @@ class PersistentMap<K : Any, V, E, out EK>(
|
||||
private val cache = NonInvalidatingUnboundCache(
|
||||
name,
|
||||
loadFunction = { key -> Optional.ofNullable(loadValue(key)) },
|
||||
removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass)
|
||||
removalListener = ExplicitRemoval(toPersistentEntityKey, persistentEntityClass),
|
||||
cacheFactory = cacheFactory
|
||||
)
|
||||
|
||||
/** Preload to allow [all] to take data only from the cache (cache is unbound) */
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.internal.post
|
||||
import net.corda.core.utilities.OpaqueBytes
|
||||
import net.corda.core.utilities.seconds
|
||||
import net.corda.node.VersionInfo
|
||||
import net.corda.node.services.config.NetworkServicesConfig
|
||||
import net.corda.nodeapi.internal.crypto.X509CertificateFactory
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Headers
|
||||
@ -19,8 +20,11 @@ import java.util.*
|
||||
import java.util.zip.ZipInputStream
|
||||
import javax.naming.ServiceUnavailableException
|
||||
|
||||
class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo: VersionInfo) : NetworkRegistrationService {
|
||||
private val registrationURL = URL("$compatibilityZoneURL/certificate")
|
||||
class HTTPNetworkRegistrationService(
|
||||
val config : NetworkServicesConfig,
|
||||
val versionInfo: VersionInfo
|
||||
) : NetworkRegistrationService {
|
||||
private val registrationURL = URL("${config.doormanURL}/certificate")
|
||||
|
||||
companion object {
|
||||
private val TRANSIENT_ERROR_STATUS_CODES = setOf(HTTP_BAD_GATEWAY, HTTP_UNAVAILABLE, HTTP_GATEWAY_TIMEOUT)
|
||||
@ -54,7 +58,8 @@ class HTTPNetworkRegistrationService(compatibilityZoneURL: URL, val versionInfo:
|
||||
override fun submitRequest(request: PKCS10CertificationRequest): String {
|
||||
return String(registrationURL.post(OpaqueBytes(request.encoded),
|
||||
"Platform-Version" to "${versionInfo.platformVersion}",
|
||||
"Client-Version" to versionInfo.releaseVersion))
|
||||
"Client-Version" to versionInfo.releaseVersion,
|
||||
"Private-Network-Map" to (config.pnm?.toString() ?: "")))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,10 +96,9 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
val requestId = try {
|
||||
submitOrResumeCertificateSigningRequest(keyPair)
|
||||
} catch (e: Exception) {
|
||||
if (e is ConnectException || e is ServiceUnavailableException || e is IOException) {
|
||||
throw NodeRegistrationException(e)
|
||||
}
|
||||
throw e
|
||||
throw if (e is ConnectException || e is ServiceUnavailableException || e is IOException) {
|
||||
NodeRegistrationException(e.message, e)
|
||||
} else e
|
||||
}
|
||||
|
||||
val certificates = try {
|
||||
@ -162,7 +161,7 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
println("Private key '$keyAlias' and certificate stored in node signing keystore.")
|
||||
}
|
||||
|
||||
private fun CertificateStore.loadOrCreateKeyPair(alias: String, privateKeyPassword: String = password): KeyPair {
|
||||
private fun CertificateStore.loadOrCreateKeyPair(alias: String, entryPassword: String = password): KeyPair {
|
||||
// Create or load self signed keypair from the key store.
|
||||
// We use the self sign certificate to store the key temporarily in the keystore while waiting for the request approval.
|
||||
if (alias !in this) {
|
||||
@ -171,11 +170,11 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACertificate(myLegalName.x500Principal, keyPair)
|
||||
// Save to the key store.
|
||||
with(value) {
|
||||
setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = privateKeyPassword)
|
||||
setPrivateKey(alias, keyPair.private, listOf(selfSignCert), keyPassword = entryPassword)
|
||||
save()
|
||||
}
|
||||
}
|
||||
return query { getCertificateAndKeyPair(alias, privateKeyPassword) }.keyPair
|
||||
return query { getCertificateAndKeyPair(alias, entryPassword) }.keyPair
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,7 +199,8 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
if (idlePeriodDuration != null) {
|
||||
Thread.sleep(idlePeriodDuration.toMillis())
|
||||
} else {
|
||||
throw NodeRegistrationException(e)
|
||||
throw NodeRegistrationException("Compatibility Zone registration service is currently unavailable, "
|
||||
+ "try again later!.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -249,10 +249,17 @@ open class NetworkRegistrationHelper(private val certificatesDirectory: Path,
|
||||
protected open fun isTlsCrlIssuerCertRequired(): Boolean = false
|
||||
}
|
||||
|
||||
class NodeRegistrationException(cause: Throwable?) : IOException("Unable to contact node registration service", cause)
|
||||
class NodeRegistrationException(
|
||||
message: String?,
|
||||
cause: Throwable?
|
||||
) : IOException(message ?: "Unable to contact node registration service", cause)
|
||||
|
||||
class NodeRegistrationHelper(private val config: NodeConfiguration, certService: NetworkRegistrationService, regConfig: NodeRegistrationOption, computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))) :
|
||||
NetworkRegistrationHelper(
|
||||
class NodeRegistrationHelper(
|
||||
private val config: NodeConfiguration,
|
||||
certService: NetworkRegistrationService,
|
||||
regConfig: NodeRegistrationOption,
|
||||
computeNextIdleDoormanConnectionPollInterval: (Duration?) -> Duration? = FixedPeriodLimitedRetrialStrategy(10, Duration.ofMinutes(1))
|
||||
) : NetworkRegistrationHelper(
|
||||
config.certificatesDirectory,
|
||||
config.signingCertificateStore,
|
||||
config.myLegalName,
|
||||
@ -274,7 +281,9 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
}
|
||||
|
||||
private fun createSSLKeystore(nodeCAKeyPair: KeyPair, certificates: List<X509Certificate>, tlsCertCrlIssuer: X500Name?) {
|
||||
config.p2pSslOptions.keyStore.get(createNew = true).update {
|
||||
val keyStore = config.p2pSslOptions.keyStore
|
||||
val certificateStore = keyStore.get(createNew = true)
|
||||
certificateStore.update {
|
||||
println("Generating SSL certificate for node messaging service.")
|
||||
val sslKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val sslCert = X509Utilities.createCertificate(
|
||||
@ -286,9 +295,9 @@ class NodeRegistrationHelper(private val config: NodeConfiguration, certService:
|
||||
crlDistPoint = config.tlsCertCrlDistPoint?.toString(),
|
||||
crlIssuer = tlsCertCrlIssuer)
|
||||
logger.info("Generated TLS certificate: $sslCert")
|
||||
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates)
|
||||
setPrivateKey(CORDA_CLIENT_TLS, sslKeyPair.private, listOf(sslCert) + certificates, certificateStore.entryPassword)
|
||||
}
|
||||
println("SSL private key and certificate stored in ${config.p2pSslOptions.keyStore.path}.")
|
||||
println("SSL private key and certificate stored in ${keyStore.path}.")
|
||||
}
|
||||
|
||||
private fun createTruststore(rootCertificate: X509Certificate) {
|
||||
|
@ -5,17 +5,6 @@
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd"
|
||||
logicalFilePath="migration/node-services.changelog-init.xml">
|
||||
|
||||
<changeSet author="R3.Corda" id="1511451595465-6">
|
||||
<createTable tableName="node_bft_committed_states">
|
||||
<column name="output_index" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="transaction_id" type="NVARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="consuming_transaction_id" type="NVARCHAR(64)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1511451595465-16">
|
||||
<createTable tableName="node_notary_committed_states">
|
||||
<column name="output_index" type="INT">
|
||||
@ -27,18 +16,6 @@
|
||||
<column name="consuming_transaction_id" type="NVARCHAR(64)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1511451595465-18">
|
||||
<createTable tableName="node_raft_committed_states">
|
||||
<column name="transaction_id" type="NVARCHAR(64)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="output_index" type="INT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="raft_log_index" type="BIGINT"/>
|
||||
<column name="consuming_transaction_id" type="NVARCHAR(64)"/>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1521131680317-17">
|
||||
<createTable tableName="node_notary_request_log">
|
||||
<column name="id" type="INT">
|
||||
@ -58,18 +35,11 @@
|
||||
</column>
|
||||
</createTable>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1511451595465-31">
|
||||
<addPrimaryKey columnNames="output_index, transaction_id" constraintName="node_bft_states_pkey"
|
||||
tableName="node_bft_committed_states"/>
|
||||
</changeSet>
|
||||
|
||||
<changeSet author="R3.Corda" id="1511451595465-41">
|
||||
<addPrimaryKey columnNames="output_index, transaction_id" constraintName="node_notary_states_pkey"
|
||||
tableName="node_notary_committed_states"/>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1511451595465-43">
|
||||
<addPrimaryKey columnNames="output_index, transaction_id" constraintName="node_raft_state_pkey"
|
||||
tableName="node_raft_committed_states"/>
|
||||
</changeSet>
|
||||
<changeSet author="R3.Corda" id="1521131680317-48">
|
||||
<addPrimaryKey columnNames="id" constraintName="node_notary_request_log_pkey"
|
||||
tableName="node_notary_request_log"/>
|
||||
|
@ -8,14 +8,4 @@
|
||||
<addPrimaryKey tableName="node_notary_committed_states" columnNames="output_index, transaction_id"
|
||||
constraintName="node_notary_states_pkey" clustered="false"/>
|
||||
</changeSet>
|
||||
<changeSet id="non-clustered_pk-bft_stae" author="R3.Corda" onValidationFail="MARK_RAN">
|
||||
<dropPrimaryKey tableName="node_bft_committed_states" constraintName="node_bft_states_pkey"/>
|
||||
<addPrimaryKey tableName="node_bft_committed_states" columnNames="output_index, transaction_id"
|
||||
constraintName="node_bft_states_pkey" clustered="false"/>
|
||||
</changeSet>
|
||||
<changeSet id="non-clustered_pk-raft_state" author="R3.Corda" onValidationFail="MARK_RAN">
|
||||
<dropPrimaryKey tableName="node_raft_committed_states" constraintName="node_raft_state_pkey"/>
|
||||
<addPrimaryKey tableName="node_raft_committed_states" columnNames="output_index, transaction_id"
|
||||
constraintName="node_raft_state_pkey" clustered="false"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -6,11 +6,6 @@
|
||||
logicalFilePath="migration/node-services.changelog-init.xml">
|
||||
|
||||
<changeSet author="R3.Corda" id="nullability">
|
||||
<addNotNullConstraint tableName="node_bft_committed_states" columnName="consuming_transaction_id" columnDataType="NVARCHAR(64)"/>
|
||||
|
||||
<addNotNullConstraint tableName="node_notary_committed_states" columnName="consuming_transaction_id" columnDataType="NVARCHAR(64)"/>
|
||||
|
||||
<addNotNullConstraint tableName="node_raft_committed_states" columnName="raft_log_index" columnDataType="BIGINT"/>
|
||||
<addNotNullConstraint tableName="node_raft_committed_states" columnName="consuming_transaction_id" columnDataType="NVARCHAR(64)"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -9,4 +9,6 @@
|
||||
<include file="migration/vault-schema.changelog-v4.xml"/>
|
||||
<include file="migration/vault-schema.changelog-pkey.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v5.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v6.xml"/>
|
||||
<include file="migration/vault-schema.changelog-v7.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
@ -0,0 +1,17 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="add_is_constraint_information_columns">
|
||||
<addColumn tableName="vault_states">
|
||||
<column name="constraint_type" type="INT" defaultValue="0">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
<addColumn tableName="vault_states">
|
||||
<column name="constraint_data" type="varbinary(563)">
|
||||
<constraints nullable="true"/>
|
||||
</column>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.1" encoding="UTF-8" standalone="no"?>
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.5.xsd">
|
||||
<changeSet author="R3.Corda" id="make_issuer_ref_nullable">
|
||||
<dropNotNullConstraint columnDataType="varbinary(512)" columnName="issuer_ref" tableName="vault_fungible_states"/>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
@ -6,11 +6,6 @@
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "--bootstrap-raft-cluster"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--clear-network-map-cache"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
@ -31,11 +26,6 @@
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--install-shell-extensions"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "--just-generate-node-info"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
@ -104,11 +94,6 @@
|
||||
required: false
|
||||
multiParam: true
|
||||
acceptableValues: []
|
||||
- parameterName: "-c"
|
||||
parameterType: "boolean"
|
||||
required: false
|
||||
multiParam: false
|
||||
acceptableValues: []
|
||||
- parameterName: "-d"
|
||||
parameterType: "java.lang.Boolean"
|
||||
required: false
|
@ -1 +0,0 @@
|
||||
key=value
|
@ -1,118 +0,0 @@
|
||||
# Copyright (c) 2007-2013 Alysson Bessani, Eduardo Alchieri, Paulo Sousa, and the authors indicated in the @author tags
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
############################################
|
||||
####### Communication Configurations #######
|
||||
############################################
|
||||
|
||||
#HMAC algorithm used to authenticate messages between processes (HmacMD5 is the default value)
|
||||
#This parameter is not currently being used being used
|
||||
#system.authentication.hmacAlgorithm = HmacSHA1
|
||||
|
||||
#Specify if the communication system should use a thread to send data (true or false)
|
||||
system.communication.useSenderThread = true
|
||||
|
||||
#Force all processes to use the same public/private keys pair and secret key. This is useful when deploying experiments
|
||||
#and benchmarks, but must not be used in production systems.
|
||||
system.communication.defaultkeys = true
|
||||
|
||||
############################################
|
||||
### Replication Algorithm Configurations ###
|
||||
############################################
|
||||
|
||||
#Number of servers in the group
|
||||
system.servers.num = %s
|
||||
|
||||
#Maximum number of faulty replicas
|
||||
system.servers.f = %s
|
||||
|
||||
#Timeout to asking for a client request
|
||||
system.totalordermulticast.timeout = 2000
|
||||
|
||||
|
||||
#Maximum batch size (in number of messages)
|
||||
system.totalordermulticast.maxbatchsize = 400
|
||||
|
||||
#Number of nonces (for non-determinism actions) generated
|
||||
system.totalordermulticast.nonces = 10
|
||||
|
||||
#if verification of leader-generated timestamps are increasing
|
||||
#it can only be used on systems in which the network clocks
|
||||
#are synchronized
|
||||
system.totalordermulticast.verifyTimestamps = false
|
||||
|
||||
#Quantity of messages that can be stored in the receive queue of the communication system
|
||||
system.communication.inQueueSize = 500000
|
||||
|
||||
# Quantity of messages that can be stored in the send queue of each replica
|
||||
system.communication.outQueueSize = 500000
|
||||
|
||||
#Set to 1 if SMaRt should use signatures, set to 0 if otherwise
|
||||
system.communication.useSignatures = 0
|
||||
|
||||
#Set to 1 if SMaRt should use MAC's, set to 0 if otherwise
|
||||
system.communication.useMACs = 1
|
||||
|
||||
#Set to 1 if SMaRt should use the standard output to display debug messages, set to 0 if otherwise
|
||||
system.debug = %s
|
||||
|
||||
#Print information about the replica when it is shutdown
|
||||
system.shutdownhook = true
|
||||
|
||||
############################################
|
||||
###### State Transfer Configurations #######
|
||||
############################################
|
||||
|
||||
#Activate the state transfer protocol ('true' to activate, 'false' to de-activate)
|
||||
system.totalordermulticast.state_transfer = false
|
||||
|
||||
#Maximum ahead-of-time message not discarded
|
||||
system.totalordermulticast.highMark = 10000
|
||||
|
||||
#Maximum ahead-of-time message not discarded when the replica is still on EID 0 (after which the state transfer is triggered)
|
||||
system.totalordermulticast.revival_highMark = 10
|
||||
|
||||
#Number of ahead-of-time messages necessary to trigger the state transfer after a request timeout occurs
|
||||
system.totalordermulticast.timeout_highMark = 200
|
||||
|
||||
############################################
|
||||
###### Log and Checkpoint Configurations ###
|
||||
############################################
|
||||
|
||||
system.totalordermulticast.log = false
|
||||
system.totalordermulticast.log_parallel = false
|
||||
system.totalordermulticast.log_to_disk = false
|
||||
system.totalordermulticast.sync_log = false
|
||||
|
||||
#Period at which BFT-SMaRt requests the state to the application (for the state transfer state protocol)
|
||||
system.totalordermulticast.checkpoint_period = 1
|
||||
system.totalordermulticast.global_checkpoint_period = 1
|
||||
|
||||
system.totalordermulticast.checkpoint_to_disk = false
|
||||
system.totalordermulticast.sync_ckp = false
|
||||
|
||||
|
||||
############################################
|
||||
###### Reconfiguration Configurations ######
|
||||
############################################
|
||||
|
||||
#Replicas ID for the initial view, separated by a comma.
|
||||
# The number of replicas in this parameter should be equal to that specified in 'system.servers.num'
|
||||
system.initial.view = %s
|
||||
|
||||
#The ID of the trust third party (TTP)
|
||||
system.ttp.id = 7002
|
||||
|
||||
#This sets if the system will function in Byzantine or crash-only mode. Set to "true" to support Byzantine faults
|
||||
system.bft = true
|
@ -19,7 +19,7 @@ import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria;
|
||||
import net.corda.finance.contracts.DealState;
|
||||
import net.corda.finance.contracts.asset.Cash;
|
||||
import net.corda.finance.schemas.CashSchemaV1;
|
||||
import net.corda.finance.schemas.SampleCashSchemaV2;
|
||||
import net.corda.finance.schemas.test.SampleCashSchemaV2;
|
||||
import net.corda.node.services.api.IdentityServiceInternal;
|
||||
import net.corda.nodeapi.internal.persistence.CordaPersistence;
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseTransaction;
|
||||
|
@ -38,10 +38,11 @@ import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.expect
|
||||
import net.corda.testing.core.expectEvents
|
||||
import net.corda.testing.core.sequence
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.internal.fromUserList
|
||||
import net.corda.testing.node.internal.InternalMockNetwork
|
||||
import net.corda.testing.node.internal.InternalMockNodeParameters
|
||||
import net.corda.testing.node.internal.TestStartedNode
|
||||
import net.corda.testing.node.internal.cordappsForPackages
|
||||
import net.corda.testing.node.testActor
|
||||
import org.apache.commons.io.IOUtils
|
||||
import org.assertj.core.api.Assertions.*
|
||||
|
@ -7,7 +7,7 @@ import net.corda.core.utilities.contextLogger
|
||||
import net.corda.core.utilities.getOrThrow
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.common.internal.relaxedThoroughness
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.node.internal.ProcessUtilities.startJavaProcess
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -12,7 +12,6 @@ import net.corda.testing.core.singleIdentity
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNodeParameters
|
||||
import net.corda.testing.node.StartedMockNode
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalStateException
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -39,15 +38,15 @@ class FlowRegistrationTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startup fails when two flows initiated by the same flow are registered`() {
|
||||
fun `succeeds when a subclass of a flow initiated by the same flow is registered`() {
|
||||
// register the same flow twice to invoke the error without causing errors in other tests
|
||||
responder.registerInitiatedFlow(Responder::class.java)
|
||||
assertThatIllegalStateException().isThrownBy { responder.registerInitiatedFlow(Responder::class.java) }
|
||||
responder.registerInitiatedFlow(Responder1::class.java)
|
||||
responder.registerInitiatedFlow(Responder1Subclassed::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a single initiated flow can be registered without error`() {
|
||||
responder.registerInitiatedFlow(Responder::class.java)
|
||||
responder.registerInitiatedFlow(Responder1::class.java)
|
||||
val result = initiator.startFlow(Initiator(responder.info.singleIdentity()))
|
||||
mockNetwork.runNetwork()
|
||||
assertNotNull(result.get())
|
||||
@ -63,7 +62,38 @@ class Initiator(val party: Party) : FlowLogic<String>() {
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private class Responder(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
private open class Responder1(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
open fun getPayload(): String {
|
||||
return "whats up"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("What's up")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private open class Responder2(val session: FlowSession) : FlowLogic<Unit>() {
|
||||
open fun getPayload(): String {
|
||||
return "whats up"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
session.send("What's up")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Initiator::class)
|
||||
private class Responder1Subclassed(session: FlowSession) : Responder1(session) {
|
||||
|
||||
override fun getPayload(): String {
|
||||
return "im subclassed! that's what's up!"
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
session.receive<String>().unwrap { it }
|
||||
|
@ -0,0 +1,110 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSession
|
||||
import net.corda.core.flows.InitiatedBy
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.node.services.config.FlowOverride
|
||||
import net.corda.node.services.config.FlowOverrideConfig
|
||||
import org.hamcrest.CoreMatchers.`is`
|
||||
import org.hamcrest.CoreMatchers.instanceOf
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
import org.mockito.Mockito
|
||||
import java.lang.IllegalStateException
|
||||
|
||||
private val marker = "This is a special marker"
|
||||
|
||||
class NodeFlowManagerTest {
|
||||
|
||||
@InitiatingFlow
|
||||
class Init : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
open class Resp(val otherSesh: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
class Resp2(val otherSesh: FlowSession) : FlowLogic<Unit>() {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
open class RespSub(sesh: FlowSession) : Resp(sesh) {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@InitiatedBy(Init::class)
|
||||
class RespSubSub(sesh: FlowSession) : RespSub(sesh) {
|
||||
override fun call() {
|
||||
TODO("not implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Test(expected = IllegalStateException::class)
|
||||
fun `should fail to validate if more than one registration with equal weight`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should allow registration of flows with different weights`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
|
||||
}
|
||||
|
||||
@Test()
|
||||
fun `should allow updating of registered responder at runtime`() {
|
||||
val nodeFlowManager = NodeFlowManager()
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
var factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
var flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSub::class.java)))
|
||||
// update
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
|
||||
factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
Assert.assertThat(flow, `is`(instanceOf(RespSubSub::class.java)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should allow an override to be specified`() {
|
||||
val nodeFlowManager = NodeFlowManager(FlowOverrideConfig(listOf(FlowOverride(Init::class.qualifiedName!!, Resp::class.qualifiedName!!))))
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, Resp2::class.java)
|
||||
nodeFlowManager.registerInitiatedFlow(Init::class.java, RespSubSub::class.java)
|
||||
nodeFlowManager.validateRegistrations()
|
||||
|
||||
val factory = nodeFlowManager.getFlowFactoryForInitiatingFlow(Init::class.java)!!
|
||||
val flow = factory.createFlow(Mockito.mock(FlowSession::class.java))
|
||||
|
||||
Assert.assertThat(flow, `is`(instanceOf(Resp::class.java)))
|
||||
}
|
||||
}
|
@ -2,4 +2,4 @@ package net.corda.node.internal
|
||||
|
||||
import net.corda.testing.CliBackwardsCompatibleTest
|
||||
|
||||
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartup::class.java)
|
||||
class NodeStartupCompatibilityTest : CliBackwardsCompatibleTest(NodeStartupCli::class.java)
|
@ -1,6 +1,8 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import net.corda.core.internal.div
|
||||
import net.corda.node.InitialRegistrationCmdLineOptions
|
||||
import net.corda.node.internal.subcommands.InitialRegistrationCli
|
||||
import net.corda.nodeapi.internal.config.UnknownConfigKeysPolicy
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.BeforeClass
|
||||
@ -11,7 +13,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class NodeStartupTest {
|
||||
private val startup = NodeStartup()
|
||||
private val startup = NodeStartupCli()
|
||||
|
||||
companion object {
|
||||
private lateinit var workingDirectory: Path
|
||||
@ -30,16 +32,14 @@ class NodeStartupTest {
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "node.conf")
|
||||
assertThat(startup.verbose).isEqualTo(false)
|
||||
assertThat(startup.loggingLevel).isEqualTo(Level.INFO)
|
||||
assertThat(startup.cmdLineOptions.nodeRegistrationOption).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.noLocalShell).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.sshdServer).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.justGenerateNodeInfo).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.justGenerateRpcSslCerts).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.bootstrapRaftCluster).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.unknownConfigKeysPolicy).isEqualTo(UnknownConfigKeysPolicy.FAIL)
|
||||
assertThat(startup.cmdLineOptions.devMode).isEqualTo(null)
|
||||
assertThat(startup.cmdLineOptions.clearNetworkMapCache).isEqualTo(false)
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "certificates" / "network-root-truststore.jks")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -47,6 +47,6 @@ class NodeStartupTest {
|
||||
CommandLine.populateCommand(startup, "--base-directory", (workingDirectory / "another-base-dir").toString())
|
||||
assertThat(startup.cmdLineOptions.baseDirectory).isEqualTo(workingDirectory / "another-base-dir")
|
||||
assertThat(startup.cmdLineOptions.configFile).isEqualTo(workingDirectory / "another-base-dir" / "node.conf")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePath).isEqualTo(workingDirectory / "another-base-dir" / "certificates" / "network-root-truststore.jks")
|
||||
assertThat(startup.cmdLineOptions.networkRootTrustStorePathParameter).isEqualTo(null)
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ import net.corda.nodeapi.internal.persistence.CordaPersistence
|
||||
import net.corda.nodeapi.internal.persistence.DatabaseConfig
|
||||
import net.corda.testing.core.ALICE_NAME
|
||||
import net.corda.testing.core.SerializationEnvironmentRule
|
||||
import net.corda.testing.internal.configureDatabase
|
||||
import net.corda.testing.internal.createNodeInfoAndSigned
|
||||
import net.corda.testing.internal.rigorousMock
|
||||
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
|
||||
@ -148,7 +149,7 @@ class NodeTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun createConfig(nodeName: CordaX500Name): NodeConfiguration {
|
||||
private fun createConfig(nodeName: CordaX500Name): NodeConfigurationImpl {
|
||||
val fakeAddress = NetworkHostAndPort("0.1.2.3", 456)
|
||||
return NodeConfigurationImpl(
|
||||
baseDirectory = temporaryFolder.root.toPath(),
|
||||
@ -166,7 +167,8 @@ class NodeTest {
|
||||
flowTimeout = FlowTimeoutConfiguration(timeout = Duration.ZERO, backoffBase = 1.0, maxRestartCount = 1),
|
||||
rpcSettings = NodeRpcSettings(address = fakeAddress, adminAddress = null, ssl = null),
|
||||
messagingServerAddress = null,
|
||||
notary = null
|
||||
notary = null,
|
||||
flowOverrides = FlowOverrideConfig(listOf())
|
||||
|
||||
)
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user