mirror of
https://github.com/corda/corda.git
synced 2025-06-21 00:23:09 +00:00
Merge remote-tracking branch 'corda-public/master'
This commit is contained in:
@ -66,10 +66,10 @@ dependencies {
|
||||
compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
|
||||
compile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
|
||||
compile "com.google.guava:guava:19.0"
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// JOpt: for command line flags.
|
||||
compile "net.sf.jopt-simple:jopt-simple:5.0.2"
|
||||
compile "net.sf.jopt-simple:jopt-simple:$jopt_simple_version"
|
||||
|
||||
// Artemis: for reliable p2p message queues.
|
||||
compile "org.apache.activemq:artemis-server:${artemis_version}"
|
||||
@ -77,7 +77,7 @@ dependencies {
|
||||
runtime "org.apache.activemq:artemis-amqp-protocol:${artemis_version}"
|
||||
|
||||
// JAnsi: for drawing things to the terminal in nicely coloured ways.
|
||||
compile "org.fusesource.jansi:jansi:1.13"
|
||||
compile "org.fusesource.jansi:jansi:$jansi_version"
|
||||
|
||||
// GraphStream: For visualisation
|
||||
testCompile "org.graphstream:gs-core:1.3"
|
||||
@ -95,7 +95,7 @@ dependencies {
|
||||
compile "org.eclipse.jetty:jetty-servlet:${jetty_version}"
|
||||
compile "org.eclipse.jetty:jetty-webapp:${jetty_version}"
|
||||
compile "javax.servlet:javax.servlet-api:3.1.0"
|
||||
compile "org.jolokia:jolokia-agent-war:2.0.0-M1"
|
||||
compile "org.jolokia:jolokia-agent-war:$jolokia_version"
|
||||
compile "commons-fileupload:commons-fileupload:1.3.2"
|
||||
|
||||
// Jersey for JAX-RS implementation for use in Jetty
|
||||
@ -121,26 +121,27 @@ dependencies {
|
||||
compile "com.google.jimfs:jimfs:1.1"
|
||||
|
||||
// TypeSafe Config: for simple and human friendly config files.
|
||||
compile "com.typesafe:config:1.3.0"
|
||||
compile "com.typesafe:config:$typesafe_config_version"
|
||||
|
||||
// Unit testing helpers.
|
||||
testCompile 'junit:junit:4.12'
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
|
||||
testCompile 'com.pholser:junit-quickcheck-core:0.6'
|
||||
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||
|
||||
// For H2 database support in persistence
|
||||
compile "com.h2database:h2:1.4.192"
|
||||
compile "com.h2database:h2:1.4.193"
|
||||
|
||||
// Exposed: Kotlin SQL library - under evaluation
|
||||
// TODO: Upgrade to Exposed 0.7 (has API changes)
|
||||
compile "org.jetbrains.exposed:exposed:0.5.0"
|
||||
|
||||
// SQL connection pooling library
|
||||
compile "com.zaxxer:HikariCP:2.4.7"
|
||||
compile "com.zaxxer:HikariCP:2.5.1"
|
||||
|
||||
// Hibernate: an object relational mapper for writing state objects to the database automatically.
|
||||
compile "org.hibernate:hibernate-core:5.2.2.Final"
|
||||
compile "org.hibernate:hibernate-java8:5.2.2.Final"
|
||||
compile "org.hibernate:hibernate-core:$hibernate_version"
|
||||
compile "org.hibernate:hibernate-java8:$hibernate_version"
|
||||
|
||||
// Capsule is a library for building independently executable fat JARs.
|
||||
compile 'co.paralleluniverse:capsule:1.0.3'
|
||||
@ -151,12 +152,12 @@ dependencies {
|
||||
compile 'io.atomix.catalyst:catalyst-netty:1.1.1'
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile 'junit:junit:4.12'
|
||||
integrationTestCompile "junit:junit:$junit_version"
|
||||
|
||||
testCompile "com.nhaarman:mockito-kotlin:0.6.1"
|
||||
testCompile "com.nhaarman:mockito-kotlin:1.1.0"
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
}
|
||||
|
@ -49,15 +49,25 @@ dependencies {
|
||||
}
|
||||
|
||||
task buildCordaJAR(type: FatCapsule, dependsOn: ['buildCertSigningRequestUtilityJAR']) {
|
||||
applicationClass 'net.corda.node.MainKt'
|
||||
applicationClass 'net.corda.node.Corda'
|
||||
archiveName "corda-${corda_version}.jar"
|
||||
applicationSource = files(project.tasks.findByName('jar'), 'build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml')
|
||||
applicationSource = files(project.tasks.findByName('jar'), '../build/classes/main/CordaCaplet.class', 'config/dev/log4j2.xml')
|
||||
|
||||
capsuleManifest {
|
||||
appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"]
|
||||
javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"]
|
||||
systemProperties['visualvm.display.name'] = 'Corda'
|
||||
minJavaVersion = '1.8.0'
|
||||
// This version is known to work and avoids earlier 8u versions that have bugs.
|
||||
minUpdateVersion['1.8'] = '102'
|
||||
caplets = ['CordaCaplet']
|
||||
|
||||
// JVM configuration:
|
||||
// - Constrain to small heap sizes to ease development on low end devices.
|
||||
// - Switch to the G1 GC which is going to be the default in Java 9 and gives low pause times/string dedup.
|
||||
//
|
||||
// If you change these flags, please also update Driver.kt
|
||||
jvmArgs = ['-Xmx200m', '-XX:+UseG1GC']
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,4 +90,4 @@ artifacts {
|
||||
publish {
|
||||
name = 'corda'
|
||||
disableDefaultJar = true
|
||||
}
|
||||
}
|
||||
|
@ -7,20 +7,23 @@ import net.corda.node.services.api.RegulatorService
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
|
||||
class DriverTests {
|
||||
companion object {
|
||||
val executorService = Executors.newScheduledThreadPool(2)
|
||||
|
||||
fun nodeMustBeUp(nodeInfo: NodeInfo) {
|
||||
val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address)
|
||||
// Check that the port is bound
|
||||
addressMustBeBound(hostAndPort)
|
||||
addressMustBeBound(executorService, hostAndPort)
|
||||
}
|
||||
|
||||
fun nodeMustBeDown(nodeInfo: NodeInfo) {
|
||||
val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address)
|
||||
// Check that the port is bound
|
||||
addressMustNotBeBound(hostAndPort)
|
||||
addressMustNotBeBound(executorService, hostAndPort)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,7 +53,7 @@ class DriverTests {
|
||||
|
||||
@Test
|
||||
fun randomFreePortAllocationWorks() {
|
||||
val nodeInfo = driver(portAllocation = PortAllocation.RandomFree()) {
|
||||
val nodeInfo = driver(portAllocation = PortAllocation.RandomFree) {
|
||||
val nodeInfo = startNode("NoService")
|
||||
nodeMustBeUp(nodeInfo.getOrThrow().nodeInfo)
|
||||
nodeInfo.getOrThrow()
|
||||
|
@ -1,181 +0,0 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.core.contracts.DummyContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.flows.NotaryError
|
||||
import net.corda.flows.NotaryException
|
||||
import net.corda.flows.NotaryFlow
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.freeLocalHostAndPort
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
// TODO: clean up and rewrite this using DriverDSL
|
||||
class DistributedNotaryTests {
|
||||
private val folderName = DateTimeFormatter
|
||||
.ofPattern("yyyyMMddHHmmss")
|
||||
.withZone(ZoneOffset.UTC)
|
||||
.format(Instant.now())
|
||||
val baseDir = "build/notaryTest/$folderName"
|
||||
val notaryName = "Notary Service"
|
||||
val clusterSize = 3
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
LogHelper.setLevel("-org.apache.activemq")
|
||||
LogHelper.setLevel(NetworkMapService::class)
|
||||
File(baseDir).deleteRecursively()
|
||||
File(baseDir).mkdirs()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
LogHelper.reset("org.apache.activemq")
|
||||
LogHelper.reset(NetworkMapService::class)
|
||||
File(baseDir).deleteRecursively()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should detect double spend`() {
|
||||
val masterNode = createNotaryCluster()
|
||||
val alice = createAliceNode(masterNode.net.myAddress)
|
||||
|
||||
val notaryParty = alice.netMapCache.getAnyNotary(RaftValidatingNotaryService.type)!!
|
||||
|
||||
val stx = run {
|
||||
val notaryNodeKeyPair = databaseTransaction(masterNode.database) { masterNode.services.notaryIdentityKey }
|
||||
val inputState = issueState(alice, notaryParty, notaryNodeKeyPair)
|
||||
val tx = TransactionType.General.Builder(notaryParty).withItems(inputState)
|
||||
val aliceKey = databaseTransaction(alice.database) { alice.services.legalIdentityKey }
|
||||
tx.signWith(aliceKey)
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
val buildFlow = { NotaryFlow.Client(stx) }
|
||||
|
||||
val firstSpend = alice.services.startFlow(buildFlow())
|
||||
firstSpend.resultFuture.getOrThrow()
|
||||
|
||||
val secondSpend = alice.services.startFlow(buildFlow())
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() }
|
||||
val error = ex.error as NotaryError.Conflict
|
||||
assertEquals(error.tx, stx.tx)
|
||||
}
|
||||
|
||||
private fun createNotaryCluster(): Node {
|
||||
val notaryClusterAddress = freeLocalHostAndPort()
|
||||
val keyPairs = (1..clusterSize).map { generateKeyPair() }
|
||||
|
||||
val notaryKeyTree = CompositeKey.Builder().addKeys(keyPairs.map { it.public.composite }).build(1)
|
||||
val notaryParty = Party(notaryName, notaryKeyTree).serialize()
|
||||
|
||||
var networkMapAddress: SingleMessageRecipient? = null
|
||||
|
||||
val cluster = keyPairs.mapIndexed { i, keyPair ->
|
||||
val dir = Paths.get(baseDir, "notaryNode$i")
|
||||
Files.createDirectories(dir)
|
||||
|
||||
val privateKeyFile = RaftValidatingNotaryService.type.id + "-private-key"
|
||||
val publicKeyFile = RaftValidatingNotaryService.type.id + "-public"
|
||||
|
||||
notaryParty.writeToFile(dir.resolve(publicKeyFile))
|
||||
keyPair.serialize().writeToFile(dir.resolve(privateKeyFile))
|
||||
|
||||
val node: Node
|
||||
if (networkMapAddress == null) {
|
||||
val config = generateConfig(dir, "node" + random63BitValue(), notaryClusterAddress)
|
||||
node = createNotaryNode(config)
|
||||
networkMapAddress = node.net.myAddress
|
||||
} else {
|
||||
val config = generateConfig(dir, "node" + random63BitValue(), freeLocalHostAndPort(), notaryClusterAddress)
|
||||
node = createNotaryNode(config, networkMapAddress)
|
||||
}
|
||||
|
||||
node
|
||||
}
|
||||
|
||||
return cluster.first()
|
||||
}
|
||||
|
||||
private fun createNotaryNode(config: FullNodeConfiguration, networkMapAddress: SingleMessageRecipient? = null): Node {
|
||||
val extraAdvertisedServices = if (networkMapAddress == null) setOf(ServiceInfo(NetworkMapService.type, "NMS")) else emptySet<ServiceInfo>()
|
||||
|
||||
val notaryNode = Node(
|
||||
configuration = config,
|
||||
advertisedServices = extraAdvertisedServices + ServiceInfo(RaftValidatingNotaryService.type, notaryName),
|
||||
networkMapAddress = networkMapAddress)
|
||||
|
||||
notaryNode.setup().start()
|
||||
thread { notaryNode.run() }
|
||||
notaryNode.networkMapRegistrationFuture.getOrThrow()
|
||||
return notaryNode
|
||||
}
|
||||
|
||||
private fun createAliceNode(networkMapAddress: SingleMessageRecipient): Node {
|
||||
val aliceDir = Paths.get(baseDir, "alice")
|
||||
val alice = Node(
|
||||
configuration = generateConfig(aliceDir, "Alice"),
|
||||
advertisedServices = setOf(),
|
||||
networkMapAddress = networkMapAddress)
|
||||
alice.setup().start()
|
||||
thread { alice.run() }
|
||||
alice.networkMapRegistrationFuture.getOrThrow()
|
||||
return alice
|
||||
}
|
||||
|
||||
private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> {
|
||||
return databaseTransaction(node.database) {
|
||||
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notary)
|
||||
tx.signWith(node.services.legalIdentityKey)
|
||||
tx.signWith(notaryKey)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateConfig(dir: Path, name: String, notaryNodeAddress: HostAndPort? = null, notaryClusterAddress: HostAndPort? = null) = FullNodeConfiguration(
|
||||
ConfigHelper.loadConfig(dir,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = mapOf(
|
||||
"myLegalName" to name,
|
||||
"basedir" to dir,
|
||||
"artemisAddress" to freeLocalHostAndPort().toString(),
|
||||
"webAddress" to freeLocalHostAndPort().toString(),
|
||||
"notaryNodeAddress" to notaryNodeAddress?.toString(),
|
||||
"notaryClusterAddresses" to (if (notaryClusterAddress == null) emptyList<String>() else listOf(notaryClusterAddress.toString()))
|
||||
)))
|
||||
}
|
@ -0,0 +1,149 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.POUNDS
|
||||
import net.corda.core.contracts.issuedBy
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.StateMachineUpdate
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.CashFlowResult
|
||||
import net.corda.node.driver.DriverBasedTest
|
||||
import net.corda.node.driver.NodeHandle
|
||||
import net.corda.node.driver.driver
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.replicate
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class DistributedServiceTests : DriverBasedTest() {
|
||||
lateinit var alice: NodeHandle
|
||||
lateinit var notaries: List<NodeHandle>
|
||||
lateinit var aliceProxy: CordaRPCOps
|
||||
lateinit var raftNotaryIdentity: Party
|
||||
lateinit var notaryStateMachines: Observable<Pair<NodeInfo, StateMachineUpdate>>
|
||||
|
||||
override fun setup() = driver {
|
||||
// Start Alice and 3 notaries in a RAFT cluster
|
||||
val clusterSize = 3
|
||||
val testUser = User("test", "test", permissions = setOf(startFlowPermission<CashFlow>()))
|
||||
val aliceFuture = startNode("Alice", rpcUsers = listOf(testUser))
|
||||
val notariesFuture = startNotaryCluster(
|
||||
"Notary",
|
||||
rpcUsers = listOf(testUser),
|
||||
clusterSize = clusterSize,
|
||||
type = RaftValidatingNotaryService.type
|
||||
)
|
||||
|
||||
alice = aliceFuture.get()
|
||||
val (notaryIdentity, notaryNodes) = notariesFuture.get()
|
||||
raftNotaryIdentity = notaryIdentity
|
||||
notaries = notaryNodes
|
||||
|
||||
assertEquals(notaries.size, clusterSize)
|
||||
assertEquals(notaries.size, notaries.map { it.nodeInfo.legalIdentity }.toSet().size)
|
||||
|
||||
// Connect to Alice and the notaries
|
||||
fun connectRpc(node: NodeHandle): CordaRPCOps {
|
||||
val client = node.rpcClientToNode()
|
||||
client.start("test", "test")
|
||||
return client.proxy()
|
||||
}
|
||||
aliceProxy = connectRpc(alice)
|
||||
val rpcClientsToNotaries = notaries.map(::connectRpc)
|
||||
notaryStateMachines = Observable.from(rpcClientsToNotaries.map { proxy ->
|
||||
proxy.stateMachinesAndUpdates().second.map { Pair(proxy.nodeIdentity(), it) }
|
||||
}).flatMap { it.onErrorResumeNext(Observable.empty()) }.bufferUntilSubscribed()
|
||||
|
||||
runTest()
|
||||
}
|
||||
|
||||
// TODO Use a dummy distributed service rather than a Raft Notary Service as this test is only about Artemis' ability
|
||||
// to handle distributed services
|
||||
@Test
|
||||
fun `requests are distributed evenly amongst the nodes`() {
|
||||
// Issue 100 pounds, then pay ourselves 50x2 pounds
|
||||
issueCash(100.POUNDS)
|
||||
|
||||
for (i in 1..50) {
|
||||
paySelf(2.POUNDS)
|
||||
}
|
||||
|
||||
// The state machines added in the notaries should map one-to-one to notarisation requests
|
||||
val notarisationsPerNotary = HashMap<Party, Int>()
|
||||
notaryStateMachines.expectEvents(isStrict = false) {
|
||||
replicate<Pair<NodeInfo, StateMachineUpdate>>(50) {
|
||||
expect(match = { it.second is StateMachineUpdate.Added }) {
|
||||
val (notary, update) = it
|
||||
update as StateMachineUpdate.Added
|
||||
notarisationsPerNotary.compute(notary.legalIdentity) { _key, number -> number?.plus(1) ?: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The distribution of requests should be very close to sg like 16/17/17 as by default artemis does round robin
|
||||
println("Notarisation distribution: $notarisationsPerNotary")
|
||||
require(notarisationsPerNotary.size == 3)
|
||||
// We allow some leeway for artemis as it doesn't always produce perfect distribution
|
||||
require(notarisationsPerNotary.values.all { it > 10 })
|
||||
}
|
||||
|
||||
// TODO This should be in RaftNotaryServiceTests
|
||||
@Test
|
||||
fun `cluster survives if a notary is killed`() {
|
||||
// Issue 100 pounds, then pay ourselves 10x5 pounds
|
||||
issueCash(100.POUNDS)
|
||||
|
||||
for (i in 1..10) {
|
||||
paySelf(5.POUNDS)
|
||||
}
|
||||
|
||||
// Now kill a notary
|
||||
with(notaries[0].process) {
|
||||
destroy()
|
||||
waitFor()
|
||||
}
|
||||
|
||||
// Pay ourselves another 20x5 pounds
|
||||
for (i in 1..20) {
|
||||
paySelf(5.POUNDS)
|
||||
}
|
||||
|
||||
val notarisationsPerNotary = HashMap<Party, Int>()
|
||||
notaryStateMachines.expectEvents(isStrict = false) {
|
||||
replicate<Pair<NodeInfo, StateMachineUpdate>>(30) {
|
||||
expect(match = { it.second is StateMachineUpdate.Added }) {
|
||||
val (notary, update) = it
|
||||
update as StateMachineUpdate.Added
|
||||
notarisationsPerNotary.compute(notary.legalIdentity) { _key, number -> number?.plus(1) ?: 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println("Notarisation distribution: $notarisationsPerNotary")
|
||||
require(notarisationsPerNotary.size == 3)
|
||||
}
|
||||
|
||||
private fun issueCash(amount: Amount<Currency>) {
|
||||
val issueHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.IssueCash(amount, OpaqueBytes.of(0), alice.nodeInfo.legalIdentity, raftNotaryIdentity))
|
||||
require(issueHandle.returnValue.toBlocking().first() is CashFlowResult.Success)
|
||||
}
|
||||
|
||||
private fun paySelf(amount: Amount<Currency>) {
|
||||
val payHandle = aliceProxy.startFlow(
|
||||
::CashFlow,
|
||||
CashCommand.PayCash(amount.issuedBy(alice.nodeInfo.legalIdentity.ref(0)), alice.nodeInfo.legalIdentity))
|
||||
require(payHandle.returnValue.toBlocking().first() is CashFlowResult.Success)
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import net.corda.core.contracts.DummyContract
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.map
|
||||
import net.corda.flows.NotaryError
|
||||
import net.corda.flows.NotaryException
|
||||
import net.corda.flows.NotaryFlow
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
|
||||
class RaftNotaryServiceTests : NodeBasedTest() {
|
||||
private val notaryName = "RAFT Notary Service"
|
||||
|
||||
@Test
|
||||
fun `detect double spend`() {
|
||||
val (masterNode, alice) = Futures.allAsList(
|
||||
startNotaryCluster(notaryName, 3).map { it.first() },
|
||||
startNode("Alice")
|
||||
).getOrThrow()
|
||||
|
||||
val notaryParty = alice.netMapCache.getNotary(notaryName)!!
|
||||
val notaryNodeKeyPair = databaseTransaction(masterNode.database) { masterNode.services.notaryIdentityKey }
|
||||
val aliceKey = databaseTransaction(alice.database) { alice.services.legalIdentityKey }
|
||||
|
||||
val inputState = issueState(alice, notaryParty, notaryNodeKeyPair)
|
||||
|
||||
val firstSpendTx = TransactionType.General.Builder(notaryParty).withItems(inputState).run {
|
||||
signWith(aliceKey)
|
||||
toSignedTransaction(false)
|
||||
}
|
||||
val firstSpend = alice.services.startFlow(NotaryFlow.Client(firstSpendTx))
|
||||
firstSpend.resultFuture.getOrThrow()
|
||||
|
||||
val secondSpendTx = TransactionType.General.Builder(notaryParty).withItems(inputState).run {
|
||||
val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity.owningKey)
|
||||
addOutputState(dummyState)
|
||||
signWith(aliceKey)
|
||||
toSignedTransaction(false)
|
||||
}
|
||||
val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx))
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() }
|
||||
val error = ex.error as NotaryError.Conflict
|
||||
assertEquals(error.tx, secondSpendTx.tx)
|
||||
}
|
||||
|
||||
private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> {
|
||||
return databaseTransaction(node.database) {
|
||||
val tx = DummyContract.generateInitial(node.info.legalIdentity.ref(0), Random().nextInt(), notary)
|
||||
tx.signWith(node.services.legalIdentityKey)
|
||||
tx.signWith(notaryKey)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0))
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
|
||||
import net.corda.testing.messaging.SimpleMQClient
|
||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* Runs the security tests with the attacker pretending to be a node on the network.
|
||||
*/
|
||||
class MQSecurityAsNodeTest : MQSecurityTest() {
|
||||
|
||||
override fun startAttacker(attacker: SimpleMQClient) {
|
||||
attacker.start(PEER_USER, PEER_USER) // Login as a peer
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to RPC requests address`() {
|
||||
assertSendAttackFails(RPC_REQUESTS_QUEUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the node running the broker can login using the special node user`() {
|
||||
val attacker = clientTo(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start(NODE_USER, NODE_USER)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login as the default cluster user`() {
|
||||
val attacker = clientTo(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQClusterSecurityException::class.java).isThrownBy {
|
||||
attacker.start(ActiveMQDefaultConfiguration.getDefaultClusterUser(), ActiveMQDefaultConfiguration.getDefaultClusterPassword())
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login without a username and password`() {
|
||||
val attacker = clientTo(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start()
|
||||
}
|
||||
}
|
||||
}
|
@ -6,7 +6,7 @@ import net.corda.testing.messaging.SimpleMQClient
|
||||
/**
|
||||
* Runs the security tests with the attacker being a valid RPC user of Alice.
|
||||
*/
|
||||
class RPCSecurityTest : MQSecurityTest() {
|
||||
class MQSecurityAsRPCTest : MQSecurityTest() {
|
||||
override val extraRPCUsers = listOf(User("evil", "pass", permissions = emptySet()))
|
||||
|
||||
override fun startAttacker(attacker: SimpleMQClient) {
|
@ -12,14 +12,17 @@ import net.corda.core.random63BitValue
|
||||
import net.corda.core.seconds
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import net.corda.node.services.config.configureTestSSL
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_ADDRESS
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.INTERNAL_PREFIX
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NETWORK_MAP_QUEUE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NOTIFICATIONS_ADDRESS
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.P2P_QUEUE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEERS_PREFIX
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_QUEUE_REMOVALS_QUEUE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
|
||||
import net.corda.node.services.messaging.CordaRPCClientImpl
|
||||
import net.corda.node.services.messaging.NodeMessagingClient.Companion.RPC_QUEUE_REMOVALS_QUEUE
|
||||
import net.corda.testing.messaging.SimpleMQClient
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQNonExistentQueueException
|
||||
@ -44,8 +47,8 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser)
|
||||
attacker = SimpleMQClient(alice.configuration.artemisAddress)
|
||||
alice = startNode("Alice", rpcUsers = extraRPCUsers + rpcUser).getOrThrow()
|
||||
attacker = clientTo(alice.configuration.artemisAddress)
|
||||
startAttacker(attacker)
|
||||
}
|
||||
|
||||
@ -70,27 +73,31 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to peer address`() {
|
||||
fun `send message to address of peer which has been communicated with`() {
|
||||
val bobParty = startBobAndCommunicateWithAlice()
|
||||
assertSendAttackFails("$PEERS_PREFIX${bobParty.owningKey.toBase58String()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for peer which has not been communciated with`() {
|
||||
val bob = startNode("Bob").getOrThrow()
|
||||
assertAllQueueCreationAttacksFail("$PEERS_PREFIX${bob.info.legalIdentity.owningKey.toBase58String()}")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create queue for unknown peer`() {
|
||||
val invalidPeerQueue = "$PEERS_PREFIX${generateKeyPair().public.composite.toBase58String()}"
|
||||
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = true)
|
||||
assertNonTempQueueCreationAttackFails(invalidPeerQueue, durable = false)
|
||||
assertTempQueueCreationAttackFails(invalidPeerQueue)
|
||||
assertAllQueueCreationAttacksFail(invalidPeerQueue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `consume message from network map queue`() {
|
||||
assertConsumeAttackFails(NETWORK_MAP_ADDRESS.toString())
|
||||
assertConsumeAttackFails(NETWORK_MAP_QUEUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to network map address`() {
|
||||
assertSendAttackFails(NETWORK_MAP_ADDRESS.toString())
|
||||
assertSendAttackFails(NETWORK_MAP_QUEUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -133,15 +140,19 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create random queue`() {
|
||||
val randomQueue = random63BitValue().toString()
|
||||
assertNonTempQueueCreationAttackFails(randomQueue, durable = false)
|
||||
assertNonTempQueueCreationAttackFails(randomQueue, durable = true)
|
||||
assertTempQueueCreationAttackFails(randomQueue)
|
||||
fun `create random internal queue`() {
|
||||
val randomQueue = "$INTERNAL_PREFIX${random63BitValue()}"
|
||||
assertAllQueueCreationAttacksFail(randomQueue)
|
||||
}
|
||||
|
||||
fun clientTo(target: HostAndPort): SimpleMQClient {
|
||||
val client = SimpleMQClient(target)
|
||||
@Test
|
||||
fun `create random queue`() {
|
||||
val randomQueue = random63BitValue().toString()
|
||||
assertAllQueueCreationAttacksFail(randomQueue)
|
||||
}
|
||||
|
||||
fun clientTo(target: HostAndPort, config: SSLConfiguration = configureTestSSL()): SimpleMQClient {
|
||||
val client = SimpleMQClient(target, config)
|
||||
clients += client
|
||||
return client
|
||||
}
|
||||
@ -164,6 +175,12 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
return rpcClient.session.addressQuery(clientQueueQuery).queueNames.single().toString()
|
||||
}
|
||||
|
||||
fun assertAllQueueCreationAttacksFail(queue: String) {
|
||||
assertNonTempQueueCreationAttackFails(queue, durable = true)
|
||||
assertNonTempQueueCreationAttackFails(queue, durable = false)
|
||||
assertTempQueueCreationAttackFails(queue)
|
||||
}
|
||||
|
||||
fun assertTempQueueCreationAttackFails(queue: String) {
|
||||
assertAttackFails(queue, "CREATE_NON_DURABLE_QUEUE") {
|
||||
attacker.session.createTemporaryQueue(queue, queue)
|
||||
@ -210,7 +227,7 @@ abstract class MQSecurityTest : NodeBasedTest() {
|
||||
}
|
||||
|
||||
private fun startBobAndCommunicateWithAlice(): Party {
|
||||
val bob = startNode("Bob")
|
||||
val bob = startNode("Bob").getOrThrow()
|
||||
bob.services.registerFlowInitiator(SendFlow::class, ::ReceiveFlow)
|
||||
val bobParty = bob.info.legalIdentity
|
||||
// Perform a protocol exchange to force the peer queue to be created
|
||||
|
@ -0,0 +1,123 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.*
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.messaging.createMessage
|
||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.flows.ServiceRequestMessage
|
||||
import net.corda.flows.sendRequest
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
import net.corda.node.services.transactions.SimpleNotaryService
|
||||
import net.corda.node.utilities.ServiceIdentityGenerator
|
||||
import net.corda.testing.freeLocalHostAndPort
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.Test
|
||||
import java.util.*
|
||||
|
||||
class P2PMessagingTest : NodeBasedTest() {
|
||||
@Test
|
||||
fun `network map will work after restart`() {
|
||||
fun startNodes() = Futures.allAsList(startNode("NodeA"), startNode("NodeB"), startNode("Notary"))
|
||||
|
||||
val startUpDuration = elapsedTime { startNodes().getOrThrow() }
|
||||
// Start the network map a second time - this will restore message queues from the journal.
|
||||
// This will hang and fail prior the fix. https://github.com/corda/corda/issues/37
|
||||
stopAllNodes()
|
||||
startNodes().getOrThrow(timeout = startUpDuration.multipliedBy(3))
|
||||
}
|
||||
|
||||
// https://github.com/corda/corda/issues/71
|
||||
@Test
|
||||
fun `communicating with a service running on the network map node`() {
|
||||
startNetworkMapNode(advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)))
|
||||
networkMapNode.respondWith("Hello")
|
||||
val alice = startNode("Alice").getOrThrow()
|
||||
val serviceAddress = alice.services.networkMapCache.run {
|
||||
alice.net.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!)
|
||||
}
|
||||
val received = alice.receiveFrom(serviceAddress).getOrThrow(10.seconds)
|
||||
assertThat(received).isEqualTo("Hello")
|
||||
}
|
||||
|
||||
// TODO Use a dummy distributed service
|
||||
@Test
|
||||
fun `communicating with a distributed service which the network map node is part of`() {
|
||||
val serviceName = "DistributedService"
|
||||
|
||||
val root = tempFolder.root.toPath()
|
||||
ServiceIdentityGenerator.generateToDisk(
|
||||
listOf(root / "NetworkMap", root / "Service Node 2"),
|
||||
RaftValidatingNotaryService.type.id,
|
||||
serviceName)
|
||||
|
||||
val distributedService = ServiceInfo(RaftValidatingNotaryService.type, serviceName)
|
||||
val notaryClusterAddress = freeLocalHostAndPort()
|
||||
startNetworkMapNode(
|
||||
"NetworkMap",
|
||||
advertisedServices = setOf(distributedService),
|
||||
configOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString()))
|
||||
val (serviceNode2, alice) = Futures.allAsList(
|
||||
startNode(
|
||||
"Service Node 2",
|
||||
advertisedServices = setOf(distributedService),
|
||||
configOverrides = mapOf(
|
||||
"notaryNodeAddress" to freeLocalHostAndPort().toString(),
|
||||
"notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))),
|
||||
startNode("Alice")
|
||||
).getOrThrow()
|
||||
|
||||
assertAllNodesAreUsed(listOf(networkMapNode, serviceNode2), serviceName, alice)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `communicating with a distributed service which we're part of`() {
|
||||
val serviceName = "Distributed Service"
|
||||
val distributedService = startNotaryCluster(serviceName, 2).getOrThrow()
|
||||
assertAllNodesAreUsed(distributedService, serviceName, distributedService[0])
|
||||
}
|
||||
|
||||
private fun assertAllNodesAreUsed(participatingServiceNodes: List<Node>, serviceName: String, originatingNode: Node) {
|
||||
// Setup each node in the distributed service to return back it's NodeInfo so that we can know which node is being used
|
||||
participatingServiceNodes.forEach { node ->
|
||||
node.respondWith(node.info)
|
||||
}
|
||||
val serviceAddress = originatingNode.services.networkMapCache.run {
|
||||
originatingNode.net.getAddressOfParty(getPartyInfo(getNotary(serviceName)!!)!!)
|
||||
}
|
||||
val participatingNodes = HashSet<Any>()
|
||||
// Try several times so that we can be fairly sure that any node not participating is not due to Artemis' selection
|
||||
// strategy. 3 attempts for each node seems to be sufficient.
|
||||
// This is not testing the distribution of the requests - DistributedServiceTests already does that
|
||||
for (it in 1..participatingServiceNodes.size * 3) {
|
||||
participatingNodes += originatingNode.receiveFrom(serviceAddress).getOrThrow(10.seconds)
|
||||
if (participatingNodes.size == participatingServiceNodes.size) {
|
||||
break
|
||||
}
|
||||
}
|
||||
assertThat(participatingNodes).containsOnlyElementsOf(participatingServiceNodes.map(Node::info))
|
||||
}
|
||||
|
||||
private fun Node.respondWith(message: Any) {
|
||||
net.addMessageHandler(javaClass.name, DEFAULT_SESSION_ID) { netMessage, reg ->
|
||||
val request = netMessage.data.deserialize<TestRequest>()
|
||||
val response = net.createMessage(javaClass.name, request.sessionID, message.serialize().bytes)
|
||||
net.send(response, request.replyTo)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Node.receiveFrom(target: MessageRecipients): ListenableFuture<Any> {
|
||||
val request = TestRequest(replyTo = net.myAddress)
|
||||
return net.sendRequest<Any>(javaClass.name, request, target)
|
||||
}
|
||||
|
||||
private data class TestRequest(override val sessionID: Long = random63BitValue(),
|
||||
override val replyTo: SingleMessageRecipient) : ServiceRequestMessage
|
||||
}
|
@ -1,53 +1,68 @@
|
||||
package net.corda.services.messaging
|
||||
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.RPC_REQUESTS_QUEUE
|
||||
import net.corda.testing.messaging.SimpleMQClient
|
||||
import org.apache.activemq.artemis.api.config.ActiveMQDefaultConfiguration
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQClusterSecurityException
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQSecurityException
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import kotlinx.support.jdk7.use
|
||||
import net.corda.core.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.flows.sendRequest
|
||||
import net.corda.node.internal.NetworkMapInfo
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.network.NetworkMapService.Companion.REGISTER_FLOW_TOPIC
|
||||
import net.corda.node.services.network.NetworkMapService.RegistrationRequest
|
||||
import net.corda.node.services.network.NodeRegistration
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.testing.TestNodeConfiguration
|
||||
import net.corda.testing.node.NodeBasedTest
|
||||
import net.corda.testing.node.SimpleNode
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
/**
|
||||
* Runs the security tests with the attacker pretending to be a node on the network.
|
||||
*/
|
||||
class P2PSecurityTest : MQSecurityTest() {
|
||||
class P2PSecurityTest : NodeBasedTest() {
|
||||
|
||||
override fun startAttacker(attacker: SimpleMQClient) {
|
||||
attacker.start(PEER_USER, PEER_USER) // Login as a peer
|
||||
@Test
|
||||
fun `incorrect legal name for the network map service config`() {
|
||||
val incorrectNetworkMapName = random63BitValue().toString()
|
||||
val node = startNode("Bob", configOverrides = mapOf(
|
||||
"networkMapService" to mapOf(
|
||||
"address" to networkMapNode.configuration.artemisAddress.toString(),
|
||||
"legalName" to incorrectNetworkMapName
|
||||
)
|
||||
))
|
||||
// The connection will be rejected as the legal name doesn't match
|
||||
assertThatThrownBy { node.getOrThrow() }.hasMessageContaining(incorrectNetworkMapName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `send message to RPC requests address`() {
|
||||
assertSendAttackFails(RPC_REQUESTS_QUEUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the node running the broker can login using the special node user`() {
|
||||
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start(NODE_USER, NODE_USER)
|
||||
fun `register with the network map service using a legal name different from the TLS CN`() {
|
||||
startSimpleNode("Attacker").use {
|
||||
// Register with the network map using a different legal name
|
||||
val response = it.registerWithNetworkMap("Legit Business")
|
||||
// We don't expect a response because the network map's host verification will prevent a connection back
|
||||
// to the attacker as the TLS CN will not match the legal name it has just provided
|
||||
assertThatExceptionOfType(TimeoutException::class.java).isThrownBy {
|
||||
response.getOrThrow(2.seconds)
|
||||
}
|
||||
}
|
||||
attacker.stop()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login as the default cluster user`() {
|
||||
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQClusterSecurityException::class.java).isThrownBy {
|
||||
attacker.start(ActiveMQDefaultConfiguration.getDefaultClusterUser(), ActiveMQDefaultConfiguration.getDefaultClusterPassword())
|
||||
}
|
||||
attacker.stop()
|
||||
private fun startSimpleNode(legalName: String): SimpleNode {
|
||||
val config = TestNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath() / legalName,
|
||||
myLegalName = legalName,
|
||||
networkMapService = NetworkMapInfo(networkMapNode.configuration.artemisAddress, networkMapNode.info.legalIdentity.name))
|
||||
config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name
|
||||
return SimpleNode(config).apply { start() }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `login without a username and password`() {
|
||||
val attacker = SimpleMQClient(alice.configuration.artemisAddress)
|
||||
assertThatExceptionOfType(ActiveMQSecurityException::class.java).isThrownBy {
|
||||
attacker.start()
|
||||
}
|
||||
attacker.stop()
|
||||
private fun SimpleNode.registerWithNetworkMap(registrationName: String): ListenableFuture<NetworkMapService.RegistrationResponse> {
|
||||
val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public))
|
||||
val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX)
|
||||
val request = RegistrationRequest(registration.toWire(identity.private), net.myAddress)
|
||||
return net.sendRequest<NetworkMapService.RegistrationResponse>(REGISTER_FLOW_TOPIC, request, networkMapNode.net.myAddress)
|
||||
}
|
||||
}
|
43
node/src/main/kotlin/net/corda/node/ArgsParser.kt
Normal file
43
node/src/main/kotlin/net/corda/node/ArgsParser.kt
Normal file
@ -0,0 +1,43 @@
|
||||
package net.corda.node
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.div
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import java.io.PrintStream
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
class ArgsParser {
|
||||
private val optionParser = OptionParser()
|
||||
// The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
|
||||
// Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
|
||||
private val baseDirectoryArg = optionParser
|
||||
.accepts("base-directory", "The node working directory where all the files are kept")
|
||||
.withRequiredArg()
|
||||
.defaultsTo(".")
|
||||
private val configFileArg = optionParser
|
||||
.accepts("config-file", "The path to the config file")
|
||||
.withRequiredArg()
|
||||
.defaultsTo("node.conf")
|
||||
private val logToConsoleArg = optionParser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
|
||||
private val helpArg = optionParser.accepts("help").forHelp()
|
||||
|
||||
fun parse(vararg args: String): CmdLineOptions {
|
||||
val optionSet = optionParser.parse(*args)
|
||||
require(!optionSet.has(baseDirectoryArg) || !optionSet.has(configFileArg)) {
|
||||
"${baseDirectoryArg.options()[0]} and ${configFileArg.options()[0]} cannot be specified together"
|
||||
}
|
||||
val baseDirectory = Paths.get(optionSet.valueOf(baseDirectoryArg)).normalize().toAbsolutePath()
|
||||
val configFile = baseDirectory / optionSet.valueOf(configFileArg)
|
||||
return CmdLineOptions(baseDirectory, configFile, optionSet.has(helpArg), optionSet.has(logToConsoleArg))
|
||||
}
|
||||
|
||||
fun printHelp(sink: PrintStream) = optionParser.printHelpOn(sink)
|
||||
}
|
||||
|
||||
data class CmdLineOptions(val baseDirectory: Path, val configFile: Path?, val help: Boolean, val logToConsole: Boolean) {
|
||||
fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map<String, Any?> = emptyMap()): Config {
|
||||
return ConfigHelper.loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides)
|
||||
}
|
||||
}
|
@ -1,21 +1,19 @@
|
||||
@file:JvmName("Corda")
|
||||
package net.corda.node
|
||||
|
||||
import com.typesafe.config.ConfigException
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.div
|
||||
import net.corda.core.randomOrNull
|
||||
import net.corda.core.rootCause
|
||||
import net.corda.core.then
|
||||
import net.corda.core.*
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.utilities.ANSIProgressObserver
|
||||
import org.fusesource.jansi.Ansi
|
||||
import org.fusesource.jansi.AnsiConsole
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.lang.management.ManagementFactory
|
||||
import java.net.InetAddress
|
||||
import java.nio.file.Paths
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
private var renderBasicInfoToConsole = true
|
||||
@ -33,30 +31,25 @@ fun printBasicNodeInfo(description: String, info: String? = null) {
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val startTime = System.currentTimeMillis()
|
||||
checkJavaVersion()
|
||||
|
||||
val parser = OptionParser()
|
||||
// The intent of allowing a command line configurable directory and config path is to allow deployment flexibility.
|
||||
// Other general configuration should live inside the config file unless we regularly need temporary overrides on the command line
|
||||
val baseDirectoryArg = parser.accepts("base-directory", "The directory to put all files under").withOptionalArg()
|
||||
val configFileArg = parser.accepts("config-file", "The path to the config file").withOptionalArg()
|
||||
val logToConsoleArg = parser.accepts("log-to-console", "If set, prints logging to the console as well as to a file.")
|
||||
val helpArg = parser.accepts("help").forHelp()
|
||||
val argsParser = ArgsParser()
|
||||
|
||||
val cmdlineOptions = try {
|
||||
parser.parse(*args)
|
||||
argsParser.parse(*args)
|
||||
} catch (ex: Exception) {
|
||||
println("Unknown command line arguments: ${ex.message}")
|
||||
exitProcess(1)
|
||||
}
|
||||
|
||||
// Maybe render command line help.
|
||||
if (cmdlineOptions.has(helpArg)) {
|
||||
parser.printHelpOn(System.out)
|
||||
if (cmdlineOptions.help) {
|
||||
argsParser.printHelp(System.out)
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
// Set up logging.
|
||||
if (cmdlineOptions.has(logToConsoleArg)) {
|
||||
if (cmdlineOptions.logToConsole) {
|
||||
// This property is referenced from the XML config file.
|
||||
System.setProperty("consoleLogLevel", "info")
|
||||
renderBasicInfoToConsole = false
|
||||
@ -64,46 +57,52 @@ fun main(args: Array<String>) {
|
||||
|
||||
drawBanner()
|
||||
|
||||
val baseDirectoryPath = if (cmdlineOptions.has(baseDirectoryArg)) Paths.get(cmdlineOptions.valueOf(baseDirectoryArg)) else Paths.get(".").normalize()
|
||||
System.setProperty("log-path", (baseDirectoryPath / "logs").toAbsolutePath().toString())
|
||||
System.setProperty("log-path", (cmdlineOptions.baseDirectory / "logs").toString())
|
||||
|
||||
val log = LoggerFactory.getLogger("Main")
|
||||
printBasicNodeInfo("Logs can be found in", System.getProperty("log-path"))
|
||||
val configFile = if (cmdlineOptions.has(configFileArg)) Paths.get(cmdlineOptions.valueOf(configFileArg)) else null
|
||||
|
||||
val conf = try {
|
||||
FullNodeConfiguration(ConfigHelper.loadConfig(baseDirectoryPath, configFile))
|
||||
FullNodeConfiguration(cmdlineOptions.baseDirectory, cmdlineOptions.loadConfig())
|
||||
} catch (e: ConfigException) {
|
||||
println("Unable to load the configuration file: ${e.rootCause.message}")
|
||||
exitProcess(2)
|
||||
}
|
||||
val dir = conf.basedir.toAbsolutePath().normalize()
|
||||
|
||||
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().getPath()}")
|
||||
log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}")
|
||||
val info = ManagementFactory.getRuntimeMXBean()
|
||||
log.info("CommandLine Args: ${info.getInputArguments().joinToString(" ")}")
|
||||
log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}")
|
||||
log.info("Application Args: ${args.joinToString(" ")}")
|
||||
log.info("bootclasspath: ${info.bootClassPath}")
|
||||
log.info("classpath: ${info.classPath}")
|
||||
log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}")
|
||||
log.info("Machine: ${InetAddress.getLocalHost().hostName}")
|
||||
log.info("Working Directory: ${dir}")
|
||||
log.info("Working Directory: ${cmdlineOptions.baseDirectory}")
|
||||
|
||||
try {
|
||||
val dirFile = dir.toFile()
|
||||
if (!dirFile.exists())
|
||||
dirFile.mkdirs()
|
||||
cmdlineOptions.baseDirectory.createDirectories()
|
||||
|
||||
val node = conf.createNode()
|
||||
|
||||
node.start()
|
||||
printPluginsAndServices(node)
|
||||
|
||||
node.networkMapRegistrationFuture.then {
|
||||
thread {
|
||||
Thread.sleep(30.seconds.toMillis())
|
||||
while (!node.networkMapRegistrationFuture.isDone) {
|
||||
printBasicNodeInfo("Waiting for response from network map ...")
|
||||
Thread.sleep(30.seconds.toMillis())
|
||||
}
|
||||
}
|
||||
|
||||
node.networkMapRegistrationFuture.success {
|
||||
val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0
|
||||
printBasicNodeInfo("Node started up and registered in $elapsed sec")
|
||||
|
||||
if (renderBasicInfoToConsole)
|
||||
ANSIProgressObserver(node.smm)
|
||||
} failure {
|
||||
log.error("Error during network map registration", it)
|
||||
exitProcess(1)
|
||||
}
|
||||
node.run()
|
||||
} catch (e: Exception) {
|
||||
@ -113,9 +112,24 @@ fun main(args: Array<String>) {
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
private fun checkJavaVersion() {
|
||||
// Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83
|
||||
try {
|
||||
Paths.get("").normalize()
|
||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||
println("""
|
||||
You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.
|
||||
Corda will now exit...""")
|
||||
exitProcess(1)
|
||||
}
|
||||
}
|
||||
|
||||
private fun printPluginsAndServices(node: Node) {
|
||||
node.configuration.extraAdvertisedServiceIds.let { if (it.isNotEmpty()) printBasicNodeInfo("Providing network services", it) }
|
||||
val plugins = node.pluginRegistries.map { it.javaClass.name }.filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") }.map { it.substringBefore('$') }
|
||||
val plugins = node.pluginRegistries
|
||||
.map { it.javaClass.name }
|
||||
.filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") }
|
||||
.map { it.substringBefore('$') }
|
||||
if (plugins.isNotEmpty())
|
||||
printBasicNodeInfo("Loaded plugins", plugins.joinToString())
|
||||
}
|
||||
@ -129,7 +143,6 @@ private fun messageOfTheDay(): Pair<String, String> {
|
||||
"\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"",
|
||||
"It's kind of like a block chain but\ncords sounded healthier than chains.",
|
||||
"Computer science and finance together.\nYou should see our crazy Christmas parties!"
|
||||
|
||||
)
|
||||
if (Emoji.hasEmojiTerminal)
|
||||
messages +=
|
||||
@ -139,6 +152,9 @@ private fun messageOfTheDay(): Pair<String, String> {
|
||||
}
|
||||
|
||||
private fun drawBanner() {
|
||||
// This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box.
|
||||
AnsiConsole.systemInstall()
|
||||
|
||||
val (msg1, msg2) = Emoji.renderIfSupported { messageOfTheDay() }
|
||||
|
||||
println(Ansi.ansi().fgBrightRed().a(
|
||||
@ -147,8 +163,6 @@ private fun drawBanner() {
|
||||
/ ____/ _________/ /___ _
|
||||
/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a(
|
||||
"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a(
|
||||
"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().
|
||||
"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold().
|
||||
a("--- DEVELOPER SNAPSHOT ------------------------------------------------------------").newline().reset())
|
||||
}
|
||||
|
||||
|
@ -5,12 +5,13 @@ package net.corda.node.driver
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigRenderOptions
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.*
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.div
|
||||
import net.corda.core.future
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
@ -18,7 +19,7 @@ import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.messaging.NodeMessagingClient
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
@ -33,7 +34,10 @@ import java.time.Instant
|
||||
import java.time.ZoneOffset.UTC
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import java.util.concurrent.TimeUnit.SECONDS
|
||||
import java.util.concurrent.TimeoutException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@ -43,11 +47,6 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
*
|
||||
* The process the driver is run in behaves as an Artemis client and starts up other processes. Namely it first
|
||||
* bootstraps a network map service to allow the specified nodes to connect to, then starts up the actual nodes.
|
||||
*
|
||||
* TODO The driver actually starts up as an Artemis server now that may route traffic. Fix this once the client MessagingService is done.
|
||||
* TODO The nodes are started up sequentially which is quite slow. Either speed up node startup or make startup parallel somehow.
|
||||
* TODO The driver now polls the network map cache for info about newly started up nodes, this could be done asynchronously(?).
|
||||
* TODO The network map service bootstrap is hacky (needs to fake the service's public key in order to retrieve the true one), needs some thought.
|
||||
*/
|
||||
|
||||
private val log: Logger = loggerFor<DriverDSL>()
|
||||
@ -68,7 +67,7 @@ interface DriverDSLExposedInterface {
|
||||
fun startNode(providedName: String? = null,
|
||||
advertisedServices: Set<ServiceInfo> = emptySet(),
|
||||
rpcUsers: List<User> = emptyList(),
|
||||
customOverrides: Map<String, Any?> = emptyMap()): Future<NodeInfoAndConfig>
|
||||
customOverrides: Map<String, Any?> = emptyMap()): ListenableFuture<NodeHandle>
|
||||
|
||||
/**
|
||||
* Starts a distributed notary cluster.
|
||||
@ -76,8 +75,14 @@ interface DriverDSLExposedInterface {
|
||||
* @param notaryName The legal name of the advertised distributed notary service.
|
||||
* @param clusterSize Number of nodes to create for the cluster.
|
||||
* @param type The advertised notary service type. Currently the only supported type is [RaftValidatingNotaryService.type].
|
||||
* @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list.
|
||||
* @return The [Party] identity of the distributed notary service, and the [NodeInfo]s of the notaries in the cluster.
|
||||
*/
|
||||
fun startNotaryCluster(notaryName: String, clusterSize: Int = 3, type: ServiceType = RaftValidatingNotaryService.type)
|
||||
fun startNotaryCluster(
|
||||
notaryName: String,
|
||||
clusterSize: Int = 3,
|
||||
type: ServiceType = RaftValidatingNotaryService.type,
|
||||
rpcUsers: List<User> = emptyList()): Future<Pair<Party, List<NodeHandle>>>
|
||||
|
||||
fun waitForAllNodesToFinish()
|
||||
}
|
||||
@ -87,7 +92,13 @@ interface DriverDSLInternalInterface : DriverDSLExposedInterface {
|
||||
fun shutdown()
|
||||
}
|
||||
|
||||
data class NodeInfoAndConfig(val nodeInfo: NodeInfo, val config: Config)
|
||||
data class NodeHandle(
|
||||
val nodeInfo: NodeInfo,
|
||||
val configuration: FullNodeConfiguration,
|
||||
val process: Process
|
||||
) {
|
||||
fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.artemisAddress, configuration)
|
||||
}
|
||||
|
||||
sealed class PortAllocation {
|
||||
abstract fun nextPort(): Int
|
||||
@ -98,7 +109,7 @@ sealed class PortAllocation {
|
||||
override fun nextPort() = portCounter.andIncrement
|
||||
}
|
||||
|
||||
class RandomFree() : PortAllocation() {
|
||||
object RandomFree : PortAllocation() {
|
||||
override fun nextPort(): Int {
|
||||
return ServerSocket().use {
|
||||
it.bind(InetSocketAddress(0))
|
||||
@ -120,7 +131,7 @@ sealed class PortAllocation {
|
||||
* Note that [DriverDSL.startNode] does not wait for the node to start up synchronously, but rather returns a [Future]
|
||||
* of the [NodeInfo] that may be waited on, which completes when the new node registered with the network map service.
|
||||
*
|
||||
* The driver implicitly bootstraps a [NetworkMapService] that may be accessed through a local cache [DriverDSL.networkMapCache].
|
||||
* The driver implicitly bootstraps a [NetworkMapService].
|
||||
*
|
||||
* @param driverDirectory The base directory node directories go into, defaults to "build/<timestamp>/". The node
|
||||
* directories themselves are "<baseDirectory>/<legalName>/", where legalName defaults to "<randomName>-<messagingPort>"
|
||||
@ -132,21 +143,19 @@ sealed class PortAllocation {
|
||||
* @param dsl The dsl itself.
|
||||
* @return The value returned in the [dsl] closure.
|
||||
*/
|
||||
|
||||
// TODO: Add an @JvmOverloads annotation
|
||||
|
||||
@JvmOverloads
|
||||
fun <A> driver(
|
||||
isDebug: Boolean = false,
|
||||
driverDirectory: Path = Paths.get("build", getTimestampAsDirectoryName()),
|
||||
portAllocation: PortAllocation = PortAllocation.Incremental(10000),
|
||||
debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005),
|
||||
useTestClock: Boolean = false,
|
||||
isDebug: Boolean = false,
|
||||
dsl: DriverDSLExposedInterface.() -> A
|
||||
) = genericDriver(
|
||||
driverDsl = DriverDSL(
|
||||
portAllocation = portAllocation,
|
||||
debugPortAllocation = debugPortAllocation,
|
||||
driverDirectory = driverDirectory,
|
||||
driverDirectory = driverDirectory.toAbsolutePath(),
|
||||
useTestClock = useTestClock,
|
||||
isDebug = isDebug
|
||||
),
|
||||
@ -176,6 +185,9 @@ fun <DI : DriverDSLExposedInterface, D : DriverDSLInternalInterface, A> genericD
|
||||
})
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook)
|
||||
return returnValue
|
||||
} catch (exception: Throwable) {
|
||||
println("Driver shutting down because of exception $exception")
|
||||
throw exception
|
||||
} finally {
|
||||
driverDsl.shutdown()
|
||||
if (shutdownHook != null) {
|
||||
@ -184,12 +196,12 @@ fun <DI : DriverDSLExposedInterface, D : DriverDSLInternalInterface, A> genericD
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTimestampAsDirectoryName(): String {
|
||||
fun getTimestampAsDirectoryName(): String {
|
||||
return DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(UTC).format(Instant.now())
|
||||
}
|
||||
|
||||
fun addressMustBeBound(hostAndPort: HostAndPort) {
|
||||
poll("address $hostAndPort to bind") {
|
||||
fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture<Unit> {
|
||||
return poll(executorService, "address $hostAndPort to bind") {
|
||||
try {
|
||||
Socket(hostAndPort.hostText, hostAndPort.port).close()
|
||||
Unit
|
||||
@ -199,8 +211,8 @@ fun addressMustBeBound(hostAndPort: HostAndPort) {
|
||||
}
|
||||
}
|
||||
|
||||
fun addressMustNotBeBound(hostAndPort: HostAndPort) {
|
||||
poll("address $hostAndPort to unbind") {
|
||||
fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture<Unit> {
|
||||
return poll(executorService, "address $hostAndPort to unbind") {
|
||||
try {
|
||||
Socket(hostAndPort.hostText, hostAndPort.port).close()
|
||||
null
|
||||
@ -210,18 +222,36 @@ fun addressMustNotBeBound(hostAndPort: HostAndPort) {
|
||||
}
|
||||
}
|
||||
|
||||
fun <A> poll(pollName: String, pollIntervalMs: Long = 500, warnCount: Int = 120, f: () -> A?): A {
|
||||
private fun <A> poll(
|
||||
executorService: ScheduledExecutorService,
|
||||
pollName: String,
|
||||
pollIntervalMs: Long = 500,
|
||||
warnCount: Int = 120,
|
||||
check: () -> A?
|
||||
): ListenableFuture<A> {
|
||||
val initialResult = check()
|
||||
val resultFuture = SettableFuture.create<A>()
|
||||
if (initialResult != null) {
|
||||
resultFuture.set(initialResult)
|
||||
return resultFuture
|
||||
}
|
||||
var counter = 0
|
||||
var result = f()
|
||||
while (result == null) {
|
||||
if (counter == warnCount) {
|
||||
log.warn("Been polling $pollName for ${pollIntervalMs * warnCount / 1000.0} seconds...")
|
||||
}
|
||||
counter = (counter % warnCount) + 1
|
||||
Thread.sleep(pollIntervalMs)
|
||||
result = f()
|
||||
fun schedulePoll() {
|
||||
executorService.schedule({
|
||||
counter++
|
||||
if (counter == warnCount) {
|
||||
log.warn("Been polling $pollName for ${pollIntervalMs * warnCount / 1000.0} seconds...")
|
||||
}
|
||||
val result = check()
|
||||
if (result == null) {
|
||||
schedulePoll()
|
||||
} else {
|
||||
resultFuture.set(result)
|
||||
}
|
||||
}, pollIntervalMs, MILLISECONDS)
|
||||
}
|
||||
return result
|
||||
schedulePoll()
|
||||
return resultFuture
|
||||
}
|
||||
|
||||
open class DriverDSL(
|
||||
@ -231,13 +261,13 @@ open class DriverDSL(
|
||||
val useTestClock: Boolean,
|
||||
val isDebug: Boolean
|
||||
) : DriverDSLInternalInterface {
|
||||
private val networkMapName = "NetworkMapService"
|
||||
private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2)
|
||||
private val networkMapLegalName = "NetworkMapService"
|
||||
private val networkMapAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
class State {
|
||||
val registeredProcesses = LinkedList<Process>()
|
||||
val registeredProcesses = LinkedList<ListenableFuture<Process>>()
|
||||
val clients = LinkedList<NodeMessagingClient>()
|
||||
var localServer: ArtemisMessagingServer? = null
|
||||
}
|
||||
|
||||
private val state = ThreadBox(State())
|
||||
@ -251,26 +281,25 @@ open class DriverDSL(
|
||||
Paths.get(quasarFileUrl.toURI()).toString()
|
||||
}
|
||||
|
||||
fun registerProcess(process: Process) = state.locked { registeredProcesses.push(process) }
|
||||
fun registerProcess(process: ListenableFuture<Process>) = state.locked { registeredProcesses.push(process) }
|
||||
|
||||
override fun waitForAllNodesToFinish() {
|
||||
state.locked {
|
||||
registeredProcesses.forEach {
|
||||
it.waitFor()
|
||||
it.getOrThrow().waitFor()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
state.locked {
|
||||
clients.forEach {
|
||||
it.stop()
|
||||
clients.forEach(NodeMessagingClient::stop)
|
||||
registeredProcesses.forEach {
|
||||
it.get().destroy()
|
||||
}
|
||||
localServer?.stop()
|
||||
registeredProcesses.forEach(Process::destroy)
|
||||
}
|
||||
/** Wait 5 seconds, then [Process.destroyForcibly] */
|
||||
val finishedFuture = future {
|
||||
val finishedFuture = executorService.submit {
|
||||
waitForAllNodesToFinish()
|
||||
}
|
||||
try {
|
||||
@ -279,16 +308,14 @@ open class DriverDSL(
|
||||
finishedFuture.cancel(true)
|
||||
state.locked {
|
||||
registeredProcesses.forEach {
|
||||
it.destroyForcibly()
|
||||
it.get().destroyForcibly()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check that we shut down properly
|
||||
state.locked {
|
||||
localServer?.run { addressMustNotBeBound(myHostPort) }
|
||||
}
|
||||
addressMustNotBeBound(networkMapAddress)
|
||||
addressMustNotBeBound(executorService, networkMapAddress).get()
|
||||
executorService.shutdown()
|
||||
}
|
||||
|
||||
private fun queryNodeInfo(webAddress: HostAndPort): NodeInfo? {
|
||||
@ -313,7 +340,7 @@ open class DriverDSL(
|
||||
}
|
||||
|
||||
override fun startNode(providedName: String?, advertisedServices: Set<ServiceInfo>,
|
||||
rpcUsers: List<User>, customOverrides: Map<String, Any?>): Future<NodeInfoAndConfig> {
|
||||
rpcUsers: List<User>, customOverrides: Map<String, Any?>): ListenableFuture<NodeHandle> {
|
||||
val messagingAddress = portAllocation.nextHostAndPort()
|
||||
val apiAddress = portAllocation.nextHostAndPort()
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
@ -322,11 +349,13 @@ open class DriverDSL(
|
||||
val baseDirectory = driverDirectory / name
|
||||
val configOverrides = mapOf(
|
||||
"myLegalName" to name,
|
||||
"basedir" to baseDirectory.normalize().toString(),
|
||||
"artemisAddress" to messagingAddress.toString(),
|
||||
"webAddress" to apiAddress.toString(),
|
||||
"extraAdvertisedServiceIds" to advertisedServices.joinToString(","),
|
||||
"networkMapAddress" to networkMapAddress.toString(),
|
||||
"networkMapService" to mapOf(
|
||||
"address" to networkMapAddress.toString(),
|
||||
"legalName" to networkMapLegalName
|
||||
),
|
||||
"useTestClock" to useTestClock,
|
||||
"rpcUsers" to rpcUsers.map {
|
||||
mapOf(
|
||||
@ -337,19 +366,28 @@ open class DriverDSL(
|
||||
}
|
||||
) + customOverrides
|
||||
|
||||
val config = ConfigHelper.loadConfig(
|
||||
baseDirectoryPath = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOverrides
|
||||
val configuration = FullNodeConfiguration(
|
||||
baseDirectory,
|
||||
ConfigHelper.loadConfig(
|
||||
baseDirectory = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = configOverrides
|
||||
)
|
||||
)
|
||||
|
||||
return future {
|
||||
registerProcess(DriverDSL.startNode(FullNodeConfiguration(config), quasarJarPath, debugPort))
|
||||
NodeInfoAndConfig(queryNodeInfo(apiAddress)!!, config)
|
||||
val startNode = startNode(executorService, configuration, quasarJarPath, debugPort)
|
||||
registerProcess(startNode)
|
||||
return startNode.map {
|
||||
NodeHandle(queryNodeInfo(apiAddress)!!, configuration, it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun startNotaryCluster(notaryName: String, clusterSize: Int, type: ServiceType) {
|
||||
override fun startNotaryCluster(
|
||||
notaryName: String,
|
||||
clusterSize: Int,
|
||||
type: ServiceType,
|
||||
rpcUsers: List<User>
|
||||
): ListenableFuture<Pair<Party, List<NodeHandle>>> {
|
||||
val nodeNames = (1..clusterSize).map { "Notary Node $it" }
|
||||
val paths = nodeNames.map { driverDirectory / it }
|
||||
ServiceIdentityGenerator.generateToDisk(paths, type.id, notaryName)
|
||||
@ -359,12 +397,19 @@ open class DriverDSL(
|
||||
val notaryClusterAddress = portAllocation.nextHostAndPort()
|
||||
|
||||
// Start the first node that will bootstrap the cluster
|
||||
startNode(nodeNames.first(), advertisedService, emptyList(), mapOf("notaryNodeAddress" to notaryClusterAddress.toString()))
|
||||
val firstNotaryFuture = startNode(nodeNames.first(), advertisedService, rpcUsers, mapOf("notaryNodeAddress" to notaryClusterAddress.toString()))
|
||||
// All other nodes will join the cluster
|
||||
nodeNames.drop(1).forEach {
|
||||
val restNotaryFutures = nodeNames.drop(1).map {
|
||||
val nodeAddress = portAllocation.nextHostAndPort()
|
||||
val configOverride = mapOf("notaryNodeAddress" to nodeAddress.toString(), "notaryClusterAddresses" to listOf(notaryClusterAddress.toString()))
|
||||
startNode(it, advertisedService, emptyList(), configOverride)
|
||||
startNode(it, advertisedService, rpcUsers, configOverride)
|
||||
}
|
||||
|
||||
return firstNotaryFuture.flatMap { firstNotary ->
|
||||
val notaryParty = firstNotary.nodeInfo.notaryIdentity
|
||||
Futures.allAsList(restNotaryFutures).map { restNotaries ->
|
||||
Pair(notaryParty, listOf(firstNotary) + restNotaries)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -372,17 +417,16 @@ open class DriverDSL(
|
||||
startNetworkMapService()
|
||||
}
|
||||
|
||||
private fun startNetworkMapService() {
|
||||
private fun startNetworkMapService(): ListenableFuture<Process> {
|
||||
val apiAddress = portAllocation.nextHostAndPort()
|
||||
val debugPort = if (isDebug) debugPortAllocation.nextPort() else null
|
||||
|
||||
val baseDirectory = driverDirectory / networkMapName
|
||||
val baseDirectory = driverDirectory / networkMapLegalName
|
||||
val config = ConfigHelper.loadConfig(
|
||||
baseDirectoryPath = baseDirectory,
|
||||
baseDirectory = baseDirectory,
|
||||
allowMissingConfig = true,
|
||||
configOverrides = mapOf(
|
||||
"myLegalName" to networkMapName,
|
||||
"basedir" to baseDirectory.normalize().toString(),
|
||||
"myLegalName" to networkMapLegalName,
|
||||
"artemisAddress" to networkMapAddress.toString(),
|
||||
"webAddress" to apiAddress.toString(),
|
||||
"extraAdvertisedServiceIds" to "",
|
||||
@ -391,11 +435,12 @@ open class DriverDSL(
|
||||
)
|
||||
|
||||
log.info("Starting network-map-service")
|
||||
registerProcess(startNode(FullNodeConfiguration(config), quasarJarPath, debugPort))
|
||||
val startNode = startNode(executorService, FullNodeConfiguration(baseDirectory, config), quasarJarPath, debugPort)
|
||||
registerProcess(startNode)
|
||||
return startNode
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val name = arrayOf(
|
||||
"Alice",
|
||||
"Bob",
|
||||
@ -405,39 +450,48 @@ open class DriverDSL(
|
||||
fun <A> pickA(array: Array<A>): A = array[Math.abs(Random().nextInt()) % array.size]
|
||||
|
||||
private fun startNode(
|
||||
executorService: ScheduledExecutorService,
|
||||
nodeConf: FullNodeConfiguration,
|
||||
quasarJarPath: String,
|
||||
debugPort: Int?
|
||||
): Process {
|
||||
): ListenableFuture<Process> {
|
||||
// Write node.conf
|
||||
writeConfig(nodeConf.basedir, "node.conf", nodeConf.config)
|
||||
writeConfig(nodeConf.baseDirectory, "node.conf", nodeConf.config)
|
||||
|
||||
val className = "net.corda.node.MainKt" // cannot directly get class for this, so just use string
|
||||
val className = "net.corda.node.Corda" // cannot directly get class for this, so just use string
|
||||
val separator = System.getProperty("file.separator")
|
||||
val classpath = System.getProperty("java.class.path")
|
||||
val path = System.getProperty("java.home") + separator + "bin" + separator + "java"
|
||||
|
||||
val debugPortArg = if (debugPort != null)
|
||||
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort")
|
||||
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort"
|
||||
else
|
||||
emptyList()
|
||||
""
|
||||
|
||||
val javaArgs = listOf(path) +
|
||||
listOf("-Dname=${nodeConf.myLegalName}", "-javaagent:$quasarJarPath") + debugPortArg +
|
||||
listOf("-cp", classpath, className) +
|
||||
"--base-directory=${nodeConf.basedir}"
|
||||
val javaArgs = listOf(
|
||||
path,
|
||||
"-Dname=${nodeConf.myLegalName}",
|
||||
"-javaagent:$quasarJarPath",
|
||||
debugPortArg,
|
||||
"-Dvisualvm.display.name=Corda",
|
||||
"-Xmx200m",
|
||||
"-XX:+UseG1GC",
|
||||
"-cp", classpath,
|
||||
className,
|
||||
"--base-directory=${nodeConf.baseDirectory}"
|
||||
).filter(String::isNotEmpty)
|
||||
val builder = ProcessBuilder(javaArgs)
|
||||
builder.redirectError(Paths.get("error.$className.log").toFile())
|
||||
builder.inheritIO()
|
||||
builder.directory(nodeConf.basedir.toFile())
|
||||
builder.directory(nodeConf.baseDirectory.toFile())
|
||||
val process = builder.start()
|
||||
addressMustBeBound(nodeConf.artemisAddress)
|
||||
// TODO There is a race condition here. Even though the messaging address is bound it may be the case that
|
||||
// the handlers for the advertised services are not yet registered. A hacky workaround is that we wait for
|
||||
// the web api address to be bound as well, as that starts after the services. Needs rethinking.
|
||||
addressMustBeBound(nodeConf.webAddress)
|
||||
|
||||
return process
|
||||
return Futures.allAsList(
|
||||
addressMustBeBound(executorService, nodeConf.artemisAddress),
|
||||
// TODO There is a race condition here. Even though the messaging address is bound it may be the case that
|
||||
// the handlers for the advertised services are not yet registered. A hacky workaround is that we wait for
|
||||
// the web api address to be bound as well, as that starts after the services. Needs rethinking.
|
||||
addressMustBeBound(executorService, nodeConf.webAddress)
|
||||
).map { process }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -446,4 +500,3 @@ fun writeConfig(path: Path, filename: String, config: Config) {
|
||||
path.toFile().mkdirs()
|
||||
File("$path/$filename").writeText(config.root().render(ConfigRenderOptions.concise()))
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,39 @@
|
||||
package net.corda.node.driver
|
||||
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
abstract class DriverBasedTest {
|
||||
private val stopDriver = CountDownLatch(1)
|
||||
private var driverThread: Thread? = null
|
||||
private lateinit var driverStarted: CountDownLatch
|
||||
|
||||
protected sealed class RunTestToken {
|
||||
internal object Token : RunTestToken()
|
||||
}
|
||||
|
||||
protected abstract fun setup(): RunTestToken
|
||||
|
||||
protected fun DriverDSLExposedInterface.runTest(): RunTestToken {
|
||||
driverStarted.countDown()
|
||||
stopDriver.await()
|
||||
return RunTestToken.Token
|
||||
}
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
driverStarted = CountDownLatch(1)
|
||||
driverThread = thread {
|
||||
setup()
|
||||
}
|
||||
driverStarted.await()
|
||||
}
|
||||
|
||||
@After
|
||||
fun stop() {
|
||||
stopDriver.countDown()
|
||||
driverThread?.join()
|
||||
}
|
||||
}
|
@ -45,10 +45,11 @@ import net.corda.node.services.statemachine.StateMachineManager
|
||||
import net.corda.node.services.transactions.*
|
||||
import net.corda.node.services.vault.CashBalanceAsMetricsObserver
|
||||
import net.corda.node.services.vault.NodeVaultService
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.AddOrRemove.ADD
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.slf4j.Logger
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
@ -72,8 +73,10 @@ import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair
|
||||
// TODO: Where this node is the initial network map service, currently no networkMapService is provided.
|
||||
// In theory the NodeInfo for the node should be passed in, instead, however currently this is constructed by the
|
||||
// AbstractNode. It should be possible to generate the NodeInfo outside of AbstractNode, so it can be passed in.
|
||||
abstract class AbstractNode(open val configuration: NodeConfiguration, val networkMapService: SingleMessageRecipient?,
|
||||
val advertisedServices: Set<ServiceInfo>, val platformClock: Clock) : SingletonSerializeAsToken() {
|
||||
abstract class AbstractNode(open val configuration: NodeConfiguration,
|
||||
val advertisedServices: Set<ServiceInfo>,
|
||||
val platformClock: Clock,
|
||||
@VisibleForTesting val busyNodeLatch: ReusableLatch = ReusableLatch()) : SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
val PRIVATE_KEY_FILE_NAME = "identity-private-key"
|
||||
val PUBLIC_IDENTITY_FILE_NAME = "identity-public"
|
||||
@ -95,6 +98,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
var networkMapSeq: Long = 1
|
||||
|
||||
protected abstract val log: Logger
|
||||
protected abstract val networkMapAddress: SingleMessageRecipient?
|
||||
|
||||
// We will run as much stuff in this single thread as possible to keep the risk of thread safety bugs low during the
|
||||
// low-performance prototyping period.
|
||||
@ -124,7 +128,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
override val monitoringService: MonitoringService = MonitoringService(MetricRegistry())
|
||||
override val flowLogicRefFactory: FlowLogicRefFactory get() = flowLogicFactory
|
||||
|
||||
override fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = smm.add(logic)
|
||||
override fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> {
|
||||
return serverThread.fetchFrom { smm.add(logic) }
|
||||
}
|
||||
|
||||
override fun registerFlowInitiator(markerClass: KClass<*>, flowFactory: (Party) -> FlowLogic<*>) {
|
||||
require(markerClass !in flowFactories) { "${markerClass.java.name} has already been used to register a flow" }
|
||||
@ -172,8 +178,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
var isPreviousCheckpointsPresent = false
|
||||
private set
|
||||
|
||||
protected val _networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create()
|
||||
/** Completes once the node has successfully registered with the network map service */
|
||||
private val _networkMapRegistrationFuture: SettableFuture<Unit> = SettableFuture.create()
|
||||
val networkMapRegistrationFuture: ListenableFuture<Unit>
|
||||
get() = _networkMapRegistrationFuture
|
||||
|
||||
@ -199,7 +205,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
|
||||
// Do all of this in a database transaction so anything that might need a connection has one.
|
||||
initialiseDatabasePersistence {
|
||||
val storageServices = initialiseStorageService(configuration.basedir)
|
||||
val storageServices = initialiseStorageService(configuration.baseDirectory)
|
||||
storage = storageServices.first
|
||||
checkpointStorage = storageServices.second
|
||||
netMapCache = InMemoryNetworkMapCache()
|
||||
@ -215,7 +221,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
keyManagement = makeKeyManagementService()
|
||||
api = APIServerImpl(this@AbstractNode)
|
||||
flowLogicFactory = initialiseFlowLogicFactory()
|
||||
scheduler = NodeSchedulerService(database, services, flowLogicFactory)
|
||||
scheduler = NodeSchedulerService(database, services, flowLogicFactory, unfinishedSchedules = busyNodeLatch)
|
||||
|
||||
val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler)
|
||||
|
||||
@ -233,7 +239,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
listOf(tokenizableServices),
|
||||
checkpointStorage,
|
||||
serverThread,
|
||||
database)
|
||||
database,
|
||||
busyNodeLatch)
|
||||
if (serverThread is ExecutorService) {
|
||||
runOnStop += Runnable {
|
||||
// We wait here, even though any in-flight messages should have been drained away because the
|
||||
@ -257,7 +264,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
}
|
||||
startMessagingService(CordaRPCOpsImpl(services, smm, database))
|
||||
runOnStop += Runnable { net.stop() }
|
||||
_networkMapRegistrationFuture.setFuture(registerWithNetworkMap())
|
||||
_networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured())
|
||||
smm.start()
|
||||
// Shut down the SMM so no Fibers are scheduled.
|
||||
runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) }
|
||||
@ -277,11 +284,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
* A service entry contains the advertised [ServiceInfo] along with the service identity. The identity *name* is
|
||||
* taken from the configuration or, if non specified, generated by combining the node's legal name and the service id.
|
||||
*/
|
||||
private fun makeServiceEntries(): List<ServiceEntry> {
|
||||
protected fun makeServiceEntries(): List<ServiceEntry> {
|
||||
return advertisedServices.map {
|
||||
val serviceId = it.type.id
|
||||
val serviceName = it.name ?: "$serviceId|${configuration.myLegalName}"
|
||||
val identity = obtainKeyPair(configuration.basedir, serviceId + "-private-key", serviceId + "-public", serviceName).first
|
||||
val identity = obtainKeyPair(configuration.baseDirectory, serviceId + "-private-key", serviceId + "-public", serviceName).first
|
||||
ServiceEntry(it, identity)
|
||||
}
|
||||
}
|
||||
@ -292,7 +299,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
private fun hasSSLCertificates(): Boolean {
|
||||
val keyStore = try {
|
||||
// This will throw exception if key file not found or keystore password is incorrect.
|
||||
X509Utilities.loadKeyStore(configuration.keyStorePath, configuration.keyStorePassword)
|
||||
X509Utilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
@ -353,7 +360,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
return serviceList
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Run any tasks that are needed to ensure the node is in a correct state before running start().
|
||||
*/
|
||||
@ -372,27 +378,43 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register this node with the network map cache, and load network map from a remote service (and register for
|
||||
* updates) if one has been supplied.
|
||||
*/
|
||||
private fun registerWithNetworkMap(): ListenableFuture<Unit> {
|
||||
require(networkMapService != null || NetworkMapService.type in advertisedServices.map { it.type }) {
|
||||
private fun registerWithNetworkMapIfConfigured(): ListenableFuture<Unit> {
|
||||
require(networkMapAddress != null || NetworkMapService.type in advertisedServices.map { it.type }) {
|
||||
"Initial network map address must indicate a node that provides a network map service"
|
||||
}
|
||||
services.networkMapCache.addNode(info)
|
||||
// In the unit test environment, we may run without any network map service sometimes.
|
||||
if (networkMapService == null && inNodeNetworkMapService == null) {
|
||||
return if (networkMapAddress == null && inNodeNetworkMapService == null) {
|
||||
services.networkMapCache.runWithoutMapService()
|
||||
return noNetworkMapConfigured()
|
||||
noNetworkMapConfigured() // TODO This method isn't needed as runWithoutMapService sets the Future in the cache
|
||||
|
||||
} else {
|
||||
registerWithNetworkMap()
|
||||
}
|
||||
return registerWithNetworkMap(networkMapService ?: info.address)
|
||||
}
|
||||
|
||||
private fun registerWithNetworkMap(networkMapServiceAddress: SingleMessageRecipient): ListenableFuture<Unit> {
|
||||
/**
|
||||
* Register this node with the network map cache, and load network map from a remote service (and register for
|
||||
* updates) if one has been supplied.
|
||||
*/
|
||||
protected open fun registerWithNetworkMap(): ListenableFuture<Unit> {
|
||||
val address = networkMapAddress ?: info.address
|
||||
// Register for updates, even if we're the one running the network map.
|
||||
updateRegistration(networkMapServiceAddress, AddOrRemove.ADD)
|
||||
return services.networkMapCache.addMapService(net, networkMapServiceAddress, true, null)
|
||||
return sendNetworkMapRegistration(address).flatMap { response ->
|
||||
check(response.success) { "The network map service rejected our registration request" }
|
||||
// This Future will complete on the same executor as sendNetworkMapRegistration, namely the one used by net
|
||||
services.networkMapCache.addMapService(net, address, true, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendNetworkMapRegistration(networkMapAddress: SingleMessageRecipient): ListenableFuture<RegistrationResponse> {
|
||||
// Register this node against the network
|
||||
val instant = platformClock.instant()
|
||||
val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val reg = NodeRegistration(info, instant.toEpochMilli(), ADD, expires)
|
||||
val legalIdentityKey = obtainLegalIdentityKey()
|
||||
val request = NetworkMapService.RegistrationRequest(reg.toWire(legalIdentityKey.private), net.myAddress)
|
||||
return net.sendRequest(REGISTER_FLOW_TOPIC, request, networkMapAddress)
|
||||
}
|
||||
|
||||
/** This is overriden by the mock node implementation to enable operation without any network map service */
|
||||
@ -402,16 +424,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
"has any other map node been configured.")
|
||||
}
|
||||
|
||||
private fun updateRegistration(networkMapAddr: SingleMessageRecipient, type: AddOrRemove): ListenableFuture<RegistrationResponse> {
|
||||
// Register this node against the network
|
||||
val instant = platformClock.instant()
|
||||
val expires = instant + NetworkMapService.DEFAULT_EXPIRATION_PERIOD
|
||||
val reg = NodeRegistration(info, instant.toEpochMilli(), type, expires)
|
||||
val legalIdentityKey = obtainLegalIdentityKey()
|
||||
val request = NetworkMapService.RegistrationRequest(reg.toWire(legalIdentityKey.private), net.myAddress)
|
||||
return net.sendRequest(REGISTER_FLOW_TOPIC, request, networkMapAddr)
|
||||
}
|
||||
|
||||
protected open fun makeKeyManagementService(): KeyManagementService = PersistentKeyManagementService(partyKeys)
|
||||
|
||||
open protected fun makeNetworkMapService() {
|
||||
@ -486,8 +498,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) =
|
||||
StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage)
|
||||
|
||||
protected fun obtainLegalIdentity(): Party = obtainKeyPair(configuration.basedir, PRIVATE_KEY_FILE_NAME, PUBLIC_IDENTITY_FILE_NAME).first
|
||||
protected fun obtainLegalIdentityKey(): KeyPair = obtainKeyPair(configuration.basedir, PRIVATE_KEY_FILE_NAME, PUBLIC_IDENTITY_FILE_NAME).second
|
||||
protected fun obtainLegalIdentity(): Party = obtainKeyPair(configuration.baseDirectory, PRIVATE_KEY_FILE_NAME, PUBLIC_IDENTITY_FILE_NAME).first
|
||||
protected fun obtainLegalIdentityKey(): KeyPair = obtainKeyPair(configuration.baseDirectory, PRIVATE_KEY_FILE_NAME, PUBLIC_IDENTITY_FILE_NAME).second
|
||||
|
||||
private fun obtainKeyPair(dir: Path, privateKeyFileName: String, publicKeyFileName: String, serviceName: String? = null): Pair<Party, KeyPair> {
|
||||
// Load the private identity key, creating it if necessary. The identity key is a long term well known key that
|
||||
@ -536,6 +548,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val netwo
|
||||
}
|
||||
|
||||
protected fun createNodeDir() {
|
||||
configuration.basedir.createDirectories()
|
||||
configuration.baseDirectory.createDirectories()
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,9 @@ import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.core.toObservable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.node.services.messaging.createRPCKryo
|
||||
import net.corda.node.services.messaging.requirePermission
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.statemachine.FlowStateMachineImpl
|
||||
import net.corda.node.services.statemachine.StateMachineManager
|
||||
@ -27,12 +25,8 @@ import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import rx.Observable
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
|
||||
/**
|
||||
* Server side implementations of RPCs available to MQ based client tools. Execution takes place on the server
|
||||
@ -46,13 +40,15 @@ class CordaRPCOpsImpl(
|
||||
override val protocolVersion: Int get() = 0
|
||||
|
||||
override fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> {
|
||||
return services.networkMapCache.track()
|
||||
return databaseTransaction(database) {
|
||||
services.networkMapCache.track()
|
||||
}
|
||||
}
|
||||
|
||||
override fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> {
|
||||
return databaseTransaction(database) {
|
||||
val (vault, updates) = services.vaultService.track()
|
||||
Pair(vault.states.toList(), updates)
|
||||
Pair(vault.states, updates)
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,11 +59,13 @@ class CordaRPCOpsImpl(
|
||||
}
|
||||
|
||||
override fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> {
|
||||
val (allStateMachines, changes) = smm.track()
|
||||
return Pair(
|
||||
allStateMachines.map { stateMachineInfoFromFlowLogic(it.id, it.logic) },
|
||||
changes.map { stateMachineUpdateFromStateMachineChange(it) }
|
||||
)
|
||||
return databaseTransaction(database) {
|
||||
val (allStateMachines, changes) = smm.track()
|
||||
Pair(
|
||||
allStateMachines.map { stateMachineInfoFromFlowLogic(it.id, it.logic) },
|
||||
changes.map { stateMachineUpdateFromStateMachineChange(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> {
|
||||
|
@ -1,15 +1,18 @@
|
||||
package net.corda.node.internal
|
||||
|
||||
import com.codahale.metrics.JmxReporter
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.div
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.flatMap
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.success
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.printBasicNodeInfo
|
||||
import net.corda.node.serialization.NodeClock
|
||||
@ -17,11 +20,11 @@ import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.RPCUserServiceImpl
|
||||
import net.corda.node.services.api.MessagingServiceInternal
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
import net.corda.node.services.messaging.CordaRPCClient
|
||||
import net.corda.node.services.messaging.NodeMessagingClient
|
||||
import net.corda.node.services.startFlowPermission
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.services.transactions.RaftUniquenessProvider
|
||||
import net.corda.node.services.transactions.RaftValidatingNotaryService
|
||||
@ -53,24 +56,21 @@ import java.util.*
|
||||
import javax.management.ObjectName
|
||||
import javax.servlet.*
|
||||
import kotlin.concurrent.thread
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
|
||||
class ConfigurationException(message: String) : Exception(message)
|
||||
|
||||
/**
|
||||
* A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub],
|
||||
* loads important data off disk and starts listening for connections.
|
||||
*
|
||||
* @param configuration This is typically loaded from a TypeSafe HOCON configuration file.
|
||||
* @param networkMapAddress An external network map service to use. Should only ever be null when creating the first
|
||||
* network map service, while bootstrapping a network.
|
||||
* @param advertisedServices The services this node advertises. This must be a subset of the services it runs,
|
||||
* but nodes are not required to advertise services they run (hence subset).
|
||||
* @param clock The clock used within the node and by all flows etc.
|
||||
*/
|
||||
class Node(override val configuration: FullNodeConfiguration, networkMapAddress: SingleMessageRecipient?,
|
||||
advertisedServices: Set<ServiceInfo>, clock: Clock = NodeClock()) : AbstractNode(configuration, networkMapAddress, advertisedServices, clock) {
|
||||
class Node(override val configuration: FullNodeConfiguration,
|
||||
advertisedServices: Set<ServiceInfo>,
|
||||
clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) {
|
||||
override val log = loggerFor<Node>()
|
||||
override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress)
|
||||
|
||||
// DISCUSSION
|
||||
//
|
||||
@ -125,25 +125,22 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
override fun makeMessagingService(): MessagingServiceInternal {
|
||||
userService = RPCUserServiceImpl(configuration)
|
||||
|
||||
val serverAddr = with(configuration) {
|
||||
val serverAddress = with(configuration) {
|
||||
messagingServerAddress ?: {
|
||||
messageBroker = ArtemisMessagingServer(this, artemisAddress, services.networkMapCache, userService)
|
||||
artemisAddress
|
||||
}()
|
||||
}
|
||||
val legalIdentity = obtainLegalIdentity()
|
||||
val myIdentityOrNullIfNetworkMapService = if (networkMapService != null) legalIdentity.owningKey else null
|
||||
return NodeMessagingClient(configuration, serverAddr, myIdentityOrNullIfNetworkMapService, serverThread, database, networkMapRegistrationFuture)
|
||||
val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null
|
||||
return NodeMessagingClient(configuration, serverAddress, myIdentityOrNullIfNetworkMapService, serverThread, database,
|
||||
networkMapRegistrationFuture)
|
||||
}
|
||||
|
||||
override fun startMessagingService(rpcOps: RPCOps) {
|
||||
// Start up the embedded MQ server
|
||||
messageBroker?.apply {
|
||||
runOnStop += Runnable { messageBroker?.stop() }
|
||||
runOnStop += Runnable { stop() }
|
||||
start()
|
||||
if (networkMapService is NetworkMapAddress) {
|
||||
bridgeToNetworkMapService(networkMapService)
|
||||
}
|
||||
}
|
||||
|
||||
// Start up the MQ client.
|
||||
@ -151,6 +148,15 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
net.start(rpcOps, userService)
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert an initial step in the registration process which will throw an exception if a non-recoverable error is
|
||||
* encountered when trying to connect to the network map node.
|
||||
*/
|
||||
override fun registerWithNetworkMap(): ListenableFuture<Unit> {
|
||||
val networkMapConnection = messageBroker?.networkMapConnectionFuture ?: Futures.immediateFuture(Unit)
|
||||
return networkMapConnection.flatMap { super.registerWithNetworkMap() }
|
||||
}
|
||||
|
||||
// TODO: add flag to enable/disable webserver
|
||||
private fun initWebServer(localRpc: CordaRPCOps): Server {
|
||||
// Note that the web server handlers will all run concurrently, and not on the node thread.
|
||||
@ -182,10 +188,10 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
httpsConfiguration.outputBufferSize = 32768
|
||||
httpsConfiguration.addCustomizer(SecureRequestCustomizer())
|
||||
val sslContextFactory = SslContextFactory()
|
||||
sslContextFactory.keyStorePath = configuration.keyStorePath.toString()
|
||||
sslContextFactory.keyStorePath = configuration.keyStoreFile.toString()
|
||||
sslContextFactory.setKeyStorePassword(configuration.keyStorePassword)
|
||||
sslContextFactory.setKeyManagerPassword(configuration.keyStorePassword)
|
||||
sslContextFactory.setTrustStorePath(configuration.trustStorePath.toString())
|
||||
sslContextFactory.setTrustStorePath(configuration.trustStoreFile.toString())
|
||||
sslContextFactory.setTrustStorePassword(configuration.trustStorePassword)
|
||||
sslContextFactory.setExcludeProtocols("SSL.*", "TLSv1", "TLSv1.1")
|
||||
sslContextFactory.setIncludeProtocols("TLSv1.2")
|
||||
@ -264,7 +270,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
override fun makeUniquenessProvider(type: ServiceType): UniquenessProvider {
|
||||
return when (type) {
|
||||
RaftValidatingNotaryService.type -> with(configuration) {
|
||||
RaftUniquenessProvider(basedir, notaryNodeAddress!!, notaryClusterAddresses, database, configuration)
|
||||
RaftUniquenessProvider(baseDirectory, notaryNodeAddress!!, notaryClusterAddresses, database, configuration)
|
||||
}
|
||||
else -> PersistentUniquenessProvider()
|
||||
}
|
||||
@ -308,32 +314,36 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
override fun start(): Node {
|
||||
alreadyRunningNodeCheck()
|
||||
super.start()
|
||||
// Only start the service API requests once the network map registration is complete
|
||||
thread(name = "WebServer") {
|
||||
networkMapRegistrationFuture.getOrThrow()
|
||||
try {
|
||||
webServer = initWebServer(connectLocalRpcAsNodeUser())
|
||||
} catch(ex: Exception) {
|
||||
// TODO: We need to decide if this is a fatal error, given the API is unavailable, or whether the API
|
||||
// is not critical and we continue anyway.
|
||||
log.error("Web server startup failed", ex)
|
||||
|
||||
// Only start the service API requests once the network map registration is successfully complete
|
||||
networkMapRegistrationFuture.success {
|
||||
// This needs to be in a seperate thread so that we can reply to our own request to become RPC clients
|
||||
thread(name = "WebServer") {
|
||||
try {
|
||||
webServer = initWebServer(connectLocalRpcAsNodeUser())
|
||||
} catch(ex: Exception) {
|
||||
// TODO: We need to decide if this is a fatal error, given the API is unavailable, or whether the API
|
||||
// is not critical and we continue anyway.
|
||||
log.error("Web server startup failed", ex)
|
||||
}
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
inDomain("net.corda").
|
||||
createsObjectNamesWith { type, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
}
|
||||
// Begin exporting our own metrics via JMX.
|
||||
JmxReporter.
|
||||
forRegistry(services.monitoringService.metrics).
|
||||
inDomain("net.corda").
|
||||
createsObjectNamesWith { type, domain, name ->
|
||||
// Make the JMX hierarchy a bit better organised.
|
||||
val category = name.substringBefore('.')
|
||||
val subName = name.substringAfter('.', "")
|
||||
if (subName == "")
|
||||
ObjectName("$domain:name=$category")
|
||||
else
|
||||
ObjectName("$domain:type=$category,name=$subName")
|
||||
}.
|
||||
build().
|
||||
start()
|
||||
}
|
||||
|
||||
shutdownThread = thread(start = false) {
|
||||
stop()
|
||||
}
|
||||
@ -383,7 +393,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
// file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already
|
||||
// exists, we try to take the file lock first before replacing it and if that fails it means we're being started
|
||||
// twice with the same directory: that's a user error and we should bail out.
|
||||
val pidPath = configuration.basedir / "process-id"
|
||||
val pidPath = configuration.baseDirectory / "process-id"
|
||||
val file = pidPath.toFile()
|
||||
if (!file.exists()) {
|
||||
file.createNewFile()
|
||||
@ -392,7 +402,7 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
val f = RandomAccessFile(file, "rw")
|
||||
val l = f.channel.tryLock()
|
||||
if (l == null) {
|
||||
log.error("It appears there is already a node running with the specified data directory ${configuration.basedir}")
|
||||
log.error("It appears there is already a node running with the specified data directory ${configuration.baseDirectory}")
|
||||
log.error("Shut that other node down and try again. It may have process ID ${file.readText()}")
|
||||
System.exit(1)
|
||||
}
|
||||
@ -405,16 +415,16 @@ class Node(override val configuration: FullNodeConfiguration, networkMapAddress:
|
||||
|
||||
// Servlet filter to wrap API requests with a database transaction.
|
||||
private class DatabaseTransactionFilter(val database: Database) : Filter {
|
||||
override fun init(filterConfig: FilterConfig?) {
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
}
|
||||
|
||||
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
|
||||
databaseTransaction(database) {
|
||||
chain.doFilter(request, response)
|
||||
}
|
||||
}
|
||||
override fun init(filterConfig: FilterConfig?) {}
|
||||
override fun destroy() {}
|
||||
}
|
||||
}
|
||||
|
||||
class ConfigurationException(message: String) : Exception(message)
|
||||
|
||||
data class NetworkMapInfo(val address: HostAndPort, val legalName: String)
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.node.services
|
||||
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
|
||||
/**
|
||||
* Service for retrieving [User] objects representing RPC users who are authorised to use the RPC system. A [User]
|
||||
@ -15,7 +15,7 @@ interface RPCUserService {
|
||||
|
||||
// TODO Store passwords as salted hashes
|
||||
// TODO Or ditch this and consider something like Apache Shiro
|
||||
class RPCUserServiceImpl(config: FullNodeConfiguration) : RPCUserService {
|
||||
class RPCUserServiceImpl(config: NodeConfiguration) : RPCUserService {
|
||||
|
||||
private val _users = config.rpcUsers.associateBy(User::username)
|
||||
|
||||
|
@ -42,7 +42,7 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
||||
abstract val schemaService: SchemaService
|
||||
|
||||
abstract override val networkService: MessagingServiceInternal
|
||||
|
||||
|
||||
/**
|
||||
* Given a list of [SignedTransaction]s, writes them to the given storage for validated transactions and then
|
||||
* sends them to the vault for further processing. This is intended for implementations to call from
|
||||
@ -64,9 +64,7 @@ abstract class ServiceHubInternal : PluginServiceHub {
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: borrowing this method from service manager work in another branch. It's required to avoid circular dependency
|
||||
* between SMM and the scheduler. That particular problem should also be resolved by the service manager work
|
||||
* itself, at which point this method would not be needed (by the scheduler).
|
||||
* Starts an already constructed flow. Note that you must be on the server thread to call this method.
|
||||
*/
|
||||
abstract fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T>
|
||||
|
||||
|
@ -25,27 +25,28 @@ import kotlin.reflect.KProperty
|
||||
import kotlin.reflect.jvm.javaType
|
||||
|
||||
object ConfigHelper {
|
||||
val log = loggerFor<ConfigHelper>()
|
||||
private val log = loggerFor<ConfigHelper>()
|
||||
|
||||
fun loadConfig(baseDirectoryPath: Path,
|
||||
fun loadConfig(baseDirectory: Path,
|
||||
configFileOverride: Path? = null,
|
||||
allowMissingConfig: Boolean = false,
|
||||
configOverrides: Map<String, Any?> = emptyMap()): Config {
|
||||
|
||||
val defaultConfig = ConfigFactory.parseResources("reference.conf", ConfigParseOptions.defaults().setAllowMissing(false))
|
||||
|
||||
val normalisedBaseDir = baseDirectoryPath.normalize()
|
||||
val configFile = (configFileOverride?.normalize() ?: normalisedBaseDir / "node.conf").toFile()
|
||||
val appConfig = ConfigFactory.parseFile(configFile, ConfigParseOptions.defaults().setAllowMissing(allowMissingConfig))
|
||||
val configFile = configFileOverride ?: baseDirectory / "node.conf"
|
||||
val appConfig = ConfigFactory.parseFile(configFile.toFile(), ConfigParseOptions.defaults().setAllowMissing(allowMissingConfig))
|
||||
|
||||
val overridesMap = HashMap<String, Any?>() // If we do require a few other command line overrides eg for a nicer development experience they would go inside this map.
|
||||
overridesMap.putAll(configOverrides)
|
||||
overridesMap["basedir"] = normalisedBaseDir.toAbsolutePath().toString()
|
||||
val overrideConfig = ConfigFactory.parseMap(overridesMap)
|
||||
val overrideConfig = ConfigFactory.parseMap(configOverrides + mapOf(
|
||||
// Add substitution values here
|
||||
"basedir" to baseDirectory.toString())
|
||||
)
|
||||
|
||||
val mergedAndResolvedConfig = overrideConfig.withFallback(appConfig).withFallback(defaultConfig).resolve()
|
||||
log.info("Config:\n ${mergedAndResolvedConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
return mergedAndResolvedConfig
|
||||
val finalConfig = overrideConfig
|
||||
.withFallback(appConfig)
|
||||
.withFallback(defaultConfig)
|
||||
.resolve()
|
||||
log.info("Config:\n${finalConfig.root().render(ConfigRenderOptions.defaults())}")
|
||||
return finalConfig
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,12 +75,9 @@ class OptionalConfig<out T>(val conf: Config, val lambda: () -> T) {
|
||||
operator fun getValue(receiver: Any, metadata: KProperty<*>): T {
|
||||
return if (conf.hasPath(metadata.name)) conf.getValue(receiver, metadata) else lambda()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun <T> Config.getOrElse(lambda: () -> T): OptionalConfig<T> {
|
||||
return OptionalConfig(this, lambda)
|
||||
}
|
||||
fun <T> Config.getOrElse(lambda: () -> T): OptionalConfig<T> = OptionalConfig(this, lambda)
|
||||
|
||||
fun Config.getProperties(path: String): Properties {
|
||||
val obj = this.getObject(path)
|
||||
@ -105,26 +103,27 @@ inline fun <reified T : Any> Config.getListOrElse(path: String, default: Config.
|
||||
*/
|
||||
fun NodeConfiguration.configureWithDevSSLCertificate() = configureDevKeyAndTrustStores(myLegalName)
|
||||
|
||||
private fun NodeSSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) {
|
||||
certificatesPath.createDirectories()
|
||||
if (!trustStorePath.exists()) {
|
||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStorePath)
|
||||
private fun SSLConfiguration.configureDevKeyAndTrustStores(myLegalName: String) {
|
||||
certificatesDirectory.createDirectories()
|
||||
if (!trustStoreFile.exists()) {
|
||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordatruststore.jks").copyTo(trustStoreFile)
|
||||
}
|
||||
if (!keyStorePath.exists()) {
|
||||
if (!keyStoreFile.exists()) {
|
||||
val caKeyStore = X509Utilities.loadKeyStore(
|
||||
javaClass.classLoader.getResourceAsStream("net/corda/node/internal/certificates/cordadevcakeys.jks"),
|
||||
"cordacadevpass")
|
||||
X509Utilities.createKeystoreForSSL(keyStorePath, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
||||
X509Utilities.createKeystoreForSSL(keyStoreFile, keyStorePassword, keyStorePassword, caKeyStore, "cordacadevkeypass", myLegalName)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Move this to CoreTestUtils.kt once we can pry this from the explorer
|
||||
fun configureTestSSL(): NodeSSLConfiguration = object : NodeSSLConfiguration {
|
||||
override val certificatesPath = Files.createTempDirectory("certs")
|
||||
@JvmOverloads
|
||||
fun configureTestSSL(legalName: String = "Mega Corp."): SSLConfiguration = object : SSLConfiguration {
|
||||
override val certificatesDirectory = Files.createTempDirectory("certs")
|
||||
override val keyStorePassword: String get() = "cordacadevpass"
|
||||
override val trustStorePassword: String get() = "trustpass"
|
||||
|
||||
init {
|
||||
configureDevKeyAndTrustStores("Mega Corp.")
|
||||
configureDevKeyAndTrustStores(legalName)
|
||||
}
|
||||
}
|
||||
|
@ -3,78 +3,89 @@ package net.corda.node.services.config
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.typesafe.config.Config
|
||||
import net.corda.core.div
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.node.internal.NetworkMapInfo
|
||||
import net.corda.node.internal.Node
|
||||
import net.corda.node.serialization.NodeClock
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.NodeMessagingClient
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.utilities.TestClock
|
||||
import java.nio.file.Path
|
||||
import java.util.*
|
||||
|
||||
interface NodeSSLConfiguration {
|
||||
interface SSLConfiguration {
|
||||
val keyStorePassword: String
|
||||
val trustStorePassword: String
|
||||
val certificatesPath: Path
|
||||
val keyStorePath: Path get() = certificatesPath / "sslkeystore.jks"
|
||||
val trustStorePath: Path get() = certificatesPath / "truststore.jks"
|
||||
val certificatesDirectory: Path
|
||||
val keyStoreFile: Path get() = certificatesDirectory / "sslkeystore.jks"
|
||||
val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks"
|
||||
}
|
||||
|
||||
interface NodeConfiguration : NodeSSLConfiguration {
|
||||
val basedir: Path
|
||||
override val certificatesPath: Path get() = basedir / "certificates"
|
||||
interface NodeConfiguration : SSLConfiguration {
|
||||
val baseDirectory: Path
|
||||
override val certificatesDirectory: Path get() = baseDirectory / "certificates"
|
||||
val myLegalName: String
|
||||
val networkMapService: NetworkMapInfo?
|
||||
val nearestCity: String
|
||||
val emailAddress: String
|
||||
val exportJMXto: String
|
||||
val dataSourceProperties: Properties get() = Properties()
|
||||
val rpcUsers: List<User> get() = emptyList()
|
||||
val devMode: Boolean
|
||||
}
|
||||
|
||||
class FullNodeConfiguration(val config: Config) : NodeConfiguration {
|
||||
override val basedir: Path by config
|
||||
/**
|
||||
* [baseDirectory] is not retrieved from the config file but rather from a command line argument.
|
||||
*/
|
||||
class FullNodeConfiguration(override val baseDirectory: Path, val config: Config) : NodeConfiguration {
|
||||
override val myLegalName: String by config
|
||||
override val nearestCity: String by config
|
||||
override val emailAddress: String by config
|
||||
override val exportJMXto: String = "http"
|
||||
override val exportJMXto: String get() = "http"
|
||||
override val keyStorePassword: String by config
|
||||
override val trustStorePassword: String by config
|
||||
override val dataSourceProperties: Properties by config
|
||||
override val devMode: Boolean by config.getOrElse { false }
|
||||
val networkMapAddress: HostAndPort? by config.getOrElse { null }
|
||||
override val networkMapService: NetworkMapInfo? = config.getOptionalConfig("networkMapService")?.run {
|
||||
NetworkMapInfo(
|
||||
HostAndPort.fromString(getString("address")),
|
||||
getString("legalName"))
|
||||
}
|
||||
override val rpcUsers: List<User> = config
|
||||
.getListOrElse<Config>("rpcUsers") { emptyList() }
|
||||
.map {
|
||||
val username = it.getString("user")
|
||||
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
||||
val password = it.getString("password")
|
||||
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet()
|
||||
User(username, password, permissions)
|
||||
}
|
||||
val useHTTPS: Boolean by config
|
||||
val artemisAddress: HostAndPort by config
|
||||
val webAddress: HostAndPort by config
|
||||
// TODO This field is slightly redundant as artemisAddress is sufficient to hold the address of the node's MQ broker.
|
||||
// Instead this should be a Boolean indicating whether that broker is an internal one started by the node or an external one
|
||||
val messagingServerAddress: HostAndPort? by config.getOrElse { null }
|
||||
val extraAdvertisedServiceIds: String by config
|
||||
val useTestClock: Boolean by config.getOrElse { false }
|
||||
val notaryNodeAddress: HostAndPort? by config.getOrElse { null }
|
||||
val notaryClusterAddresses: List<HostAndPort> = config.getListOrElse<String>("notaryClusterAddresses") { emptyList<String>() }.map { HostAndPort.fromString(it) }
|
||||
val rpcUsers: List<User> =
|
||||
config.getListOrElse<Config>("rpcUsers") { emptyList() }
|
||||
.map {
|
||||
val username = it.getString("user")
|
||||
require(username.matches("\\w+".toRegex())) { "Username $username contains invalid characters" }
|
||||
val password = it.getString("password")
|
||||
val permissions = it.getListOrElse<String>("permissions") { emptyList() }.toSet()
|
||||
User(username, password, permissions)
|
||||
}
|
||||
val notaryClusterAddresses: List<HostAndPort> = config
|
||||
.getListOrElse<String>("notaryClusterAddresses") { emptyList() }
|
||||
.map { HostAndPort.fromString(it) }
|
||||
|
||||
fun createNode(): Node {
|
||||
// This is a sanity feature do not remove.
|
||||
require(!useTestClock || devMode) { "Cannot use test clock outside of dev mode" }
|
||||
|
||||
val advertisedServices = mutableSetOf<ServiceInfo>()
|
||||
if (!extraAdvertisedServiceIds.isNullOrEmpty()) {
|
||||
for (serviceId in extraAdvertisedServiceIds.split(",")) {
|
||||
advertisedServices.add(ServiceInfo.parse(serviceId))
|
||||
}
|
||||
}
|
||||
if (networkMapAddress == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
|
||||
val networkMapMessageAddress: SingleMessageRecipient? = if (networkMapAddress == null) null else NodeMessagingClient.makeNetworkMapAddress(networkMapAddress!!)
|
||||
return Node(this, networkMapMessageAddress, advertisedServices, if (useTestClock == true) TestClock() else NodeClock())
|
||||
val advertisedServices = extraAdvertisedServiceIds
|
||||
.split(",")
|
||||
.filter(String::isNotBlank)
|
||||
.map { ServiceInfo.parse(it) }
|
||||
.toMutableSet()
|
||||
if (networkMapService == null) advertisedServices.add(ServiceInfo(NetworkMapService.type))
|
||||
|
||||
return Node(this, advertisedServices, if (useTestClock) TestClock() else NodeClock())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Config.getOptionalConfig(path: String): Config? = if (hasPath(path)) getConfig(path) else null
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.events
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import kotlinx.support.jdk8.collections.compute
|
||||
import net.corda.core.ThreadBox
|
||||
@ -48,7 +47,8 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
class NodeSchedulerService(private val database: Database,
|
||||
private val services: ServiceHubInternal,
|
||||
private val flowLogicRefFactory: FlowLogicRefFactory,
|
||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor())
|
||||
private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(),
|
||||
private val unfinishedSchedules: ReusableLatch = ReusableLatch())
|
||||
: SchedulerService, SingletonSerializeAsToken() {
|
||||
|
||||
private val log = loggerFor<NodeSchedulerService>()
|
||||
@ -89,9 +89,6 @@ class NodeSchedulerService(private val database: Database,
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
|
||||
@VisibleForTesting
|
||||
val unfinishedSchedules = ReusableLatch()
|
||||
|
||||
// We need the [StateMachineManager] to be constructed before this is called in case it schedules a flow.
|
||||
fun start() {
|
||||
mutex.locked {
|
||||
@ -124,10 +121,10 @@ class NodeSchedulerService(private val database: Database,
|
||||
val removedAction = scheduledStates.remove(ref)
|
||||
if (removedAction != null) {
|
||||
unfinishedSchedules.countDown()
|
||||
}
|
||||
if (removedAction == earliestState && removedAction != null) {
|
||||
recomputeEarliest()
|
||||
rescheduleWakeUp()
|
||||
if (removedAction == earliestState) {
|
||||
recomputeEarliest()
|
||||
rescheduleWakeUp()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -147,7 +144,7 @@ class NodeSchedulerService(private val database: Database,
|
||||
Pair(earliestState, rescheduled!!)
|
||||
}
|
||||
if (scheduledState != null) {
|
||||
schedulerTimerExecutor.execute() {
|
||||
schedulerTimerExecutor.execute {
|
||||
log.trace { "Scheduling as next $scheduledState" }
|
||||
// This will block the scheduler single thread until the scheduled time (returns false) OR
|
||||
// the Future is cancelled due to rescheduling (returns true).
|
||||
@ -155,7 +152,7 @@ class NodeSchedulerService(private val database: Database,
|
||||
log.trace { "Invoking as next $scheduledState" }
|
||||
onTimeReached(scheduledState)
|
||||
} else {
|
||||
log.trace { "Recheduled $scheduledState" }
|
||||
log.trace { "Rescheduled $scheduledState" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -182,6 +179,7 @@ class NodeSchedulerService(private val database: Database,
|
||||
val scheduledLogic: FlowLogic<*>? = getScheduledLogic()
|
||||
if (scheduledLogic != null) {
|
||||
subFlow(scheduledLogic)
|
||||
scheduler.unfinishedSchedules.countDown()
|
||||
}
|
||||
}
|
||||
|
||||
@ -215,10 +213,7 @@ class NodeSchedulerService(private val database: Database,
|
||||
// TODO: FlowLogicRefFactory needs to sort out the class loader etc
|
||||
val logic = scheduler.flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef)
|
||||
logger.trace { "Scheduler starting FlowLogic $logic" }
|
||||
// FlowLogic will be checkpointed by the time this returns.
|
||||
//scheduler.services.startFlowAndForget(logic)
|
||||
scheduledLogic = logic
|
||||
scheduler.unfinishedSchedules.countDown()
|
||||
null
|
||||
}
|
||||
} else {
|
||||
|
@ -14,7 +14,7 @@ import net.corda.node.services.api.ServiceHubInternal
|
||||
class ScheduledActivityObserver(val services: ServiceHubInternal) {
|
||||
init {
|
||||
services.vaultService.rawUpdates.subscribe { update ->
|
||||
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it) }
|
||||
update.consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) }
|
||||
update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) }
|
||||
}
|
||||
}
|
||||
|
@ -3,25 +3,25 @@ package net.corda.node.services.messaging
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.messaging.MessageRecipientGroup
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.read
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Inbound
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
|
||||
import org.apache.activemq.artemis.api.core.TransportConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyAcceptorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.TransportConstants
|
||||
import java.nio.file.FileSystems
|
||||
import java.nio.file.Path
|
||||
import java.security.KeyStore
|
||||
|
||||
/**
|
||||
* The base class for Artemis services that defines shared data structures and transport configuration
|
||||
* The base class for Artemis services that defines shared data structures and SSL transport configuration.
|
||||
*/
|
||||
abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
|
||||
|
||||
companion object {
|
||||
init {
|
||||
System.setProperty("org.jboss.logging.provider", "slf4j")
|
||||
@ -34,13 +34,15 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
|
||||
|
||||
const val INTERNAL_PREFIX = "internal."
|
||||
const val PEERS_PREFIX = "${INTERNAL_PREFIX}peers."
|
||||
const val SERVICES_PREFIX = "${INTERNAL_PREFIX}services."
|
||||
const val CLIENTS_PREFIX = "clients."
|
||||
const val P2P_QUEUE = "p2p.inbound"
|
||||
const val RPC_REQUESTS_QUEUE = "rpc.requests"
|
||||
const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
|
||||
const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications"
|
||||
const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap"
|
||||
|
||||
@JvmStatic
|
||||
val NETWORK_MAP_ADDRESS = SimpleString("${INTERNAL_PREFIX}networkmap")
|
||||
const val VERIFY_PEER_COMMON_NAME = "corda.verifyPeerCommonName"
|
||||
|
||||
/**
|
||||
* Assuming the passed in target address is actually an ArtemisAddress will extract the host and port of the node. This should
|
||||
@ -50,34 +52,60 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
|
||||
@JvmStatic
|
||||
@VisibleForTesting
|
||||
fun toHostAndPort(target: MessageRecipients): HostAndPort {
|
||||
val addr = target as? ArtemisMessagingComponent.ArtemisAddress ?: throw IllegalArgumentException("Not an Artemis address")
|
||||
val addr = target as? ArtemisMessagingComponent.ArtemisPeerAddress ?: throw IllegalArgumentException("Not an Artemis address")
|
||||
return addr.hostAndPort
|
||||
}
|
||||
}
|
||||
|
||||
protected interface ArtemisAddress {
|
||||
val queueName: SimpleString
|
||||
interface ArtemisAddress : MessageRecipients {
|
||||
val queueName: String
|
||||
}
|
||||
|
||||
interface ArtemisPeerAddress : ArtemisAddress, SingleMessageRecipient {
|
||||
val hostAndPort: HostAndPort
|
||||
}
|
||||
|
||||
data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
|
||||
override val queueName: SimpleString get() = NETWORK_MAP_ADDRESS
|
||||
data class NetworkMapAddress(override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisPeerAddress {
|
||||
override val queueName: String get() = NETWORK_MAP_QUEUE
|
||||
}
|
||||
|
||||
/**
|
||||
* This is the class used to implement [SingleMessageRecipient], for now. Note that in future this class
|
||||
* may change or evolve and code that relies upon it being a simple host/port may not function correctly.
|
||||
* For instance it may contain onion routing data.
|
||||
*
|
||||
* [NodeAddress] identifies a specific peer node and an associated queue. The queue may be the peer's own queue or
|
||||
* an advertised service's queue.
|
||||
*
|
||||
* @param queueName The name of the queue this address is associated with.
|
||||
* @param hostAndPort The address of the node.
|
||||
*/
|
||||
data class NodeAddress(val identity: CompositeKey, override val hostAndPort: HostAndPort) : SingleMessageRecipient, ArtemisAddress {
|
||||
override val queueName: SimpleString = SimpleString("$PEERS_PREFIX${identity.toBase58String()}")
|
||||
override fun toString(): String = "${javaClass.simpleName}(identity = $queueName, $hostAndPort)"
|
||||
data class NodeAddress(override val queueName: String, override val hostAndPort: HostAndPort) : ArtemisPeerAddress {
|
||||
companion object {
|
||||
fun asPeer(peerIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
|
||||
return NodeAddress("$PEERS_PREFIX${peerIdentity.toBase58String()}", hostAndPort)
|
||||
}
|
||||
fun asService(serviceIdentity: CompositeKey, hostAndPort: HostAndPort): NodeAddress {
|
||||
return NodeAddress("$SERVICES_PREFIX${serviceIdentity.toBase58String()}", hostAndPort)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [ServiceAddress] implements [MessageRecipientGroup]. It holds a queue associated with a service advertised by
|
||||
* zero or more nodes. Each advertising node has an associated consumer.
|
||||
*
|
||||
* By sending to such an address Artemis will pick a consumer (uses Round Robin by default) and sends the message
|
||||
* there. We use this to establish sessions involving service counterparties.
|
||||
*
|
||||
* @param identity The service identity's owning key.
|
||||
*/
|
||||
data class ServiceAddress(val identity: CompositeKey) : ArtemisAddress, MessageRecipientGroup {
|
||||
override val queueName: String = "$SERVICES_PREFIX${identity.toBase58String()}"
|
||||
}
|
||||
|
||||
/** The config object is used to pass in the passwords for the certificate KeyStore and TrustStore */
|
||||
abstract val config: NodeSSLConfiguration
|
||||
|
||||
protected enum class ConnectionDirection { INBOUND, OUTBOUND }
|
||||
abstract val config: SSLConfiguration?
|
||||
|
||||
// Restrict enabled Cipher Suites to AES and GCM as minimum for the bulk cipher.
|
||||
// Our self-generated certificates all use ECDSA for handshakes, but we allow classical RSA certificates to work
|
||||
@ -97,52 +125,62 @@ abstract class ArtemisMessagingComponent() : SingletonSerializeAsToken() {
|
||||
* unfortunately Artemis tends to bury the exception when the password is wrong.
|
||||
*/
|
||||
fun checkStorePasswords() {
|
||||
config.keyStorePath.read {
|
||||
val config = config ?: return
|
||||
config.keyStoreFile.read {
|
||||
KeyStore.getInstance("JKS").load(it, config.keyStorePassword.toCharArray())
|
||||
}
|
||||
config.trustStorePath.read {
|
||||
config.trustStoreFile.read {
|
||||
KeyStore.getInstance("JKS").load(it, config.trustStorePassword.toCharArray())
|
||||
}
|
||||
}
|
||||
|
||||
protected fun tcpTransport(direction: ConnectionDirection, host: String, port: Int): TransportConfiguration {
|
||||
config.keyStorePath.expectedOnDefaultFileSystem()
|
||||
config.trustStorePath.expectedOnDefaultFileSystem()
|
||||
return TransportConfiguration(
|
||||
when (direction) {
|
||||
ConnectionDirection.INBOUND -> NettyAcceptorFactory::class.java.name
|
||||
ConnectionDirection.OUTBOUND -> NettyConnectorFactory::class.java.name
|
||||
},
|
||||
mapOf(
|
||||
// Basic TCP target details
|
||||
TransportConstants.HOST_PROP_NAME to host,
|
||||
TransportConstants.PORT_PROP_NAME to port,
|
||||
val config = config
|
||||
val options = mutableMapOf<String, Any?>(
|
||||
// Basic TCP target details
|
||||
TransportConstants.HOST_PROP_NAME to host,
|
||||
TransportConstants.PORT_PROP_NAME to port,
|
||||
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats
|
||||
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP",
|
||||
|
||||
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
|
||||
// and AES encryption
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStorePath,
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStorePath,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true
|
||||
|
||||
// TODO: Set up the connector's host name verifier logic to ensure we connect to the expected node even in case of MITM or BGP hijacks
|
||||
)
|
||||
// Turn on AMQP support, which needs the protocol jar on the classpath.
|
||||
// Unfortunately we cannot disable core protocol as artemis only uses AMQP for interop
|
||||
// It does not use AMQP messages for its own messages e.g. topology and heartbeats
|
||||
// TODO further investigate how to ensure we use a well defined wire level protocol for Node to Node communications
|
||||
TransportConstants.PROTOCOLS_PROP_NAME to "CORE,AMQP"
|
||||
)
|
||||
|
||||
if (config != null) {
|
||||
config.keyStoreFile.expectedOnDefaultFileSystem()
|
||||
config.trustStoreFile.expectedOnDefaultFileSystem()
|
||||
val tlsOptions = mapOf<String, Any?>(
|
||||
// Enable TLS transport layer with client certs and restrict to at least SHA256 in handshake
|
||||
// and AES encryption
|
||||
TransportConstants.SSL_ENABLED_PROP_NAME to true,
|
||||
TransportConstants.KEYSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.KEYSTORE_PATH_PROP_NAME to config.keyStoreFile,
|
||||
TransportConstants.KEYSTORE_PASSWORD_PROP_NAME to config.keyStorePassword, // TODO proper management of keystores and password
|
||||
TransportConstants.TRUSTSTORE_PROVIDER_PROP_NAME to "JKS",
|
||||
TransportConstants.TRUSTSTORE_PATH_PROP_NAME to config.trustStoreFile,
|
||||
TransportConstants.TRUSTSTORE_PASSWORD_PROP_NAME to config.trustStorePassword,
|
||||
TransportConstants.ENABLED_CIPHER_SUITES_PROP_NAME to CIPHER_SUITES.joinToString(","),
|
||||
TransportConstants.ENABLED_PROTOCOLS_PROP_NAME to "TLSv1.2",
|
||||
TransportConstants.NEED_CLIENT_AUTH_PROP_NAME to true,
|
||||
VERIFY_PEER_COMMON_NAME to (direction as? Outbound)?.expectedCommonName
|
||||
)
|
||||
options.putAll(tlsOptions)
|
||||
}
|
||||
val factoryName = when (direction) {
|
||||
is Inbound -> NettyAcceptorFactory::class.java.name
|
||||
is Outbound -> VerifyingNettyConnectorFactory::class.java.name
|
||||
}
|
||||
return TransportConfiguration(factoryName, options)
|
||||
}
|
||||
|
||||
protected fun Path.expectedOnDefaultFileSystem() {
|
||||
require(fileSystem == FileSystems.getDefault()) { "Artemis only uses the default file system" }
|
||||
}
|
||||
|
||||
protected sealed class ConnectionDirection {
|
||||
object Inbound : ConnectionDirection()
|
||||
class Outbound(val expectedCommonName: String? = null) : ConnectionDirection()
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,58 @@
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import io.netty.handler.ssl.SslHandler
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.crypto.AddressFormatException
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.div
|
||||
import net.corda.core.minutes
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.utilities.debug
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.printBasicNodeInfo
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.INBOUND
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.OUTBOUND
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.NODE_ROLE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer.NodeLoginModule.Companion.RPC_ROLE
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.CLIENTS_PREFIX
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.PEER_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Inbound
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.PEER_ROLE
|
||||
import net.corda.node.services.messaging.NodeLoginModule.Companion.RPC_ROLE
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.apache.activemq.artemis.core.config.BridgeConfiguration
|
||||
import org.apache.activemq.artemis.core.config.Configuration
|
||||
import org.apache.activemq.artemis.core.config.CoreQueueConfiguration
|
||||
import org.apache.activemq.artemis.core.config.impl.ConfigurationImpl
|
||||
import org.apache.activemq.artemis.core.config.impl.SecurityConfiguration
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnection
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnector
|
||||
import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactory
|
||||
import org.apache.activemq.artemis.core.security.Role
|
||||
import org.apache.activemq.artemis.core.server.ActiveMQServer
|
||||
import org.apache.activemq.artemis.core.server.impl.ActiveMQServerImpl
|
||||
import org.apache.activemq.artemis.spi.core.remoting.*
|
||||
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.CertificateCallback
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.RolePrincipal
|
||||
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import rx.Subscription
|
||||
import java.io.IOException
|
||||
import java.math.BigInteger
|
||||
import java.security.Principal
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executor
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import javax.annotation.concurrent.ThreadSafe
|
||||
import javax.security.auth.Subject
|
||||
import javax.security.auth.callback.CallbackHandler
|
||||
@ -50,6 +64,7 @@ import javax.security.auth.login.AppConfigurationEntry.LoginModuleControlFlag.RE
|
||||
import javax.security.auth.login.FailedLoginException
|
||||
import javax.security.auth.login.LoginException
|
||||
import javax.security.auth.spi.LoginModule
|
||||
import javax.security.cert.X509Certificate
|
||||
|
||||
// TODO: Verify that nobody can connect to us and fiddle with our config over the socket due to the secman.
|
||||
// TODO: Implement a discovery engine that can trigger builds of new connections when another node registers? (later)
|
||||
@ -79,16 +94,28 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
|
||||
private val mutex = ThreadBox(InnerState())
|
||||
private lateinit var activeMQServer: ActiveMQServer
|
||||
private val _networkMapConnectionFuture = config.networkMapService?.let { SettableFuture.create<Unit>() }
|
||||
/**
|
||||
* A [ListenableFuture] which completes when the server successfully connects to the network map node. If a
|
||||
* non-recoverable error is encountered then the Future will complete with an exception.
|
||||
*/
|
||||
val networkMapConnectionFuture: SettableFuture<Unit>? get() = _networkMapConnectionFuture
|
||||
private var networkChangeHandle: Subscription? = null
|
||||
|
||||
init {
|
||||
config.basedir.expectedOnDefaultFileSystem()
|
||||
config.baseDirectory.expectedOnDefaultFileSystem()
|
||||
}
|
||||
|
||||
/**
|
||||
* The server will make sure the bridge exists on network map changes, see method [updateBridgesOnNetworkChange]
|
||||
* We assume network map will be updated accordingly when the client node register with the network map server.
|
||||
*/
|
||||
fun start() = mutex.locked {
|
||||
if (!running) {
|
||||
configureAndStartServer()
|
||||
networkChangeHandle = networkMapCache.changed.subscribe { destroyPossibleStaleBridge(it) }
|
||||
// Deploy bridge to the network map service
|
||||
config.networkMapService?.let { deployBridge(NetworkMapAddress(it.address), it.legalName) }
|
||||
networkChangeHandle = networkMapCache.changed.subscribe { updateBridgesOnNetworkChange(it) }
|
||||
running = true
|
||||
}
|
||||
}
|
||||
@ -100,23 +127,6 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
running = false
|
||||
}
|
||||
|
||||
fun bridgeToNetworkMapService(networkMapService: NetworkMapAddress) {
|
||||
val query = activeMQServer.queueQuery(NETWORK_MAP_ADDRESS)
|
||||
if (!query.isExists) {
|
||||
activeMQServer.createQueue(NETWORK_MAP_ADDRESS, NETWORK_MAP_ADDRESS, null, true, false)
|
||||
}
|
||||
maybeDeployBridgeForAddress(networkMapService)
|
||||
}
|
||||
|
||||
private fun destroyPossibleStaleBridge(change: MapChange) {
|
||||
val staleNodeInfo = when (change) {
|
||||
is MapChange.Modified -> change.previousNode
|
||||
is MapChange.Removed -> change.node
|
||||
is MapChange.Added -> return
|
||||
}
|
||||
(staleNodeInfo.address as? ArtemisAddress)?.let { maybeDestroyBridge(it.queueName) }
|
||||
}
|
||||
|
||||
private fun configureAndStartServer() {
|
||||
val config = createArtemisConfig()
|
||||
val securityManager = createArtemisSecurityManager()
|
||||
@ -125,40 +135,19 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
registerActivationFailureListener { exception -> throw exception }
|
||||
// Some types of queue might need special preparation on our side, like dialling back or preparing
|
||||
// a lazily initialised subsystem.
|
||||
registerPostQueueCreationCallback { deployBridgeFromNewPeerQueue(it) }
|
||||
registerPostQueueCreationCallback { deployBridgesFromNewQueue(it.toString()) }
|
||||
registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } }
|
||||
}
|
||||
activeMQServer.start()
|
||||
printBasicNodeInfo("Node listening on address", myHostPort.toString())
|
||||
}
|
||||
|
||||
private fun deployBridgeFromNewPeerQueue(queueName: SimpleString) {
|
||||
log.debug { "Queue created: $queueName" }
|
||||
if (!queueName.startsWith(PEERS_PREFIX)) return
|
||||
try {
|
||||
val identity = CompositeKey.parseFromBase58(queueName.substring(PEERS_PREFIX.length))
|
||||
val nodeInfo = networkMapCache.getNodeByCompositeKey(identity)
|
||||
if (nodeInfo != null) {
|
||||
val address = nodeInfo.address
|
||||
if (address is NodeAddress) {
|
||||
maybeDeployBridgeForAddress(address)
|
||||
} else {
|
||||
log.error("Don't know how to deal with $address")
|
||||
}
|
||||
} else {
|
||||
log.error("Queue created for a peer that we don't know from the network map: $queueName")
|
||||
}
|
||||
} catch (e: AddressFormatException) {
|
||||
log.error("Flow violation: Could not parse queue name as Base 58: $queueName")
|
||||
}
|
||||
}
|
||||
|
||||
private fun createArtemisConfig(): Configuration = ConfigurationImpl().apply {
|
||||
val artemisDir = config.basedir / "artemis"
|
||||
val artemisDir = config.baseDirectory / "artemis"
|
||||
bindingsDirectory = (artemisDir / "bindings").toString()
|
||||
journalDirectory = (artemisDir / "journal").toString()
|
||||
largeMessagesDirectory = (artemisDir / "large-messages").toString()
|
||||
acceptorConfigurations = setOf(tcpTransport(INBOUND, "0.0.0.0", myHostPort.port))
|
||||
acceptorConfigurations = setOf(tcpTransport(Inbound, "0.0.0.0", myHostPort.port))
|
||||
// Enable built in message deduplication. Note we still have to do our own as the delayed commits
|
||||
// and our own definition of commit mean that the built in deduplication cannot remove all duplicates.
|
||||
idCacheSize = 2000 // Artemis Default duplicate cache size i.e. a guess
|
||||
@ -167,12 +156,32 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
managementNotificationAddress = SimpleString(NOTIFICATIONS_ADDRESS)
|
||||
// Artemis allows multiple servers to be grouped together into a cluster for load balancing purposes. The cluster
|
||||
// user is used for connecting the nodes together. It has super-user privileges and so it's imperative that its
|
||||
// password is changed from the default (as warned in the docs). Since we don't need this feature we turn it off
|
||||
// password be changed from the default (as warned in the docs). Since we don't need this feature we turn it off
|
||||
// by having its password be an unknown securely random 128-bit value.
|
||||
clusterPassword = BigInteger(128, newSecureRandom()).toString(16)
|
||||
queueConfigurations = listOf(
|
||||
queueConfig(NETWORK_MAP_QUEUE, durable = true),
|
||||
queueConfig(P2P_QUEUE, durable = true),
|
||||
// Create an RPC queue: this will service locally connected clients only (not via a bridge) and those
|
||||
// clients must have authenticated. We could use a single consumer for everything and perhaps we should,
|
||||
// but these queues are not worth persisting.
|
||||
queueConfig(RPC_REQUESTS_QUEUE, durable = false),
|
||||
// The custom name for the queue is intentional - we may wish other things to subscribe to the
|
||||
// NOTIFICATIONS_ADDRESS with different filters in future
|
||||
queueConfig(RPC_QUEUE_REMOVALS_QUEUE, address = NOTIFICATIONS_ADDRESS, filter = "_AMQ_NotifType = 1", durable = false)
|
||||
)
|
||||
configureAddressSecurity()
|
||||
}
|
||||
|
||||
private fun queueConfig(name: String, address: String = name, filter: String? = null, durable: Boolean): CoreQueueConfiguration {
|
||||
return CoreQueueConfiguration().apply {
|
||||
this.name = name
|
||||
this.address = address
|
||||
filterString = filter
|
||||
isDurable = durable
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticated clients connecting to us fall in one of three groups:
|
||||
* 1. The node hosting us and any of its logically connected components. These are given full access to all valid queues.
|
||||
@ -201,57 +210,140 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
}
|
||||
|
||||
private fun createArtemisSecurityManager(): ActiveMQJAASSecurityManager {
|
||||
val ourRootCAPublicKey = X509Utilities
|
||||
.loadCertificateFromKeyStore(config.trustStorePath, config.trustStorePassword, CORDA_ROOT_CA)
|
||||
.publicKey
|
||||
val ourPublicKey = X509Utilities
|
||||
.loadCertificateFromKeyStore(config.keyStorePath, config.keyStorePassword, CORDA_CLIENT_CA)
|
||||
val rootCAPublicKey = X509Utilities
|
||||
.loadCertificateFromKeyStore(config.trustStoreFile, config.trustStorePassword, CORDA_ROOT_CA)
|
||||
.publicKey
|
||||
val ourCertificate = X509Utilities
|
||||
.loadCertificateFromKeyStore(config.keyStoreFile, config.keyStorePassword, CORDA_CLIENT_CA)
|
||||
val ourSubjectDN = X500Name(ourCertificate.subjectDN.name)
|
||||
// This is a sanity check and should not fail unless things have been misconfigured
|
||||
require(ourSubjectDN.commonName == config.myLegalName) {
|
||||
"Legal name does not match with our subject CN: $ourSubjectDN"
|
||||
}
|
||||
val securityConfig = object : SecurityConfiguration() {
|
||||
// Override to make it work with our login module
|
||||
override fun getAppConfigurationEntry(name: String): Array<AppConfigurationEntry> {
|
||||
val options = mapOf(
|
||||
RPCUserService::class.java.name to userService,
|
||||
CORDA_ROOT_CA to ourRootCAPublicKey,
|
||||
CORDA_CLIENT_CA to ourPublicKey)
|
||||
CORDA_ROOT_CA to rootCAPublicKey,
|
||||
CORDA_CLIENT_CA to ourCertificate.publicKey)
|
||||
return arrayOf(AppConfigurationEntry(name, REQUIRED, options))
|
||||
}
|
||||
}
|
||||
return ActiveMQJAASSecurityManager(NodeLoginModule::class.java.name, securityConfig)
|
||||
}
|
||||
|
||||
private fun connectorExists(hostAndPort: HostAndPort) = hostAndPort.toString() in activeMQServer.configuration.connectorConfigurations
|
||||
private fun deployBridgesFromNewQueue(queueName: String) {
|
||||
log.debug { "Queue created: $queueName, deploying bridge(s)" }
|
||||
|
||||
private fun addConnector(hostAndPort: HostAndPort) = activeMQServer.configuration.addConnectorConfiguration(
|
||||
hostAndPort.toString(),
|
||||
tcpTransport(OUTBOUND, hostAndPort.hostText, hostAndPort.port)
|
||||
)
|
||||
|
||||
private fun bridgeExists(name: SimpleString) = activeMQServer.clusterManager.bridges.containsKey(name.toString())
|
||||
|
||||
private fun maybeDeployBridgeForAddress(address: ArtemisAddress) {
|
||||
if (!connectorExists(address.hostAndPort)) {
|
||||
addConnector(address.hostAndPort)
|
||||
fun deployBridgeToPeer(nodeInfo: NodeInfo) {
|
||||
log.debug("Deploying bridge for $queueName to $nodeInfo")
|
||||
val address = nodeInfo.address
|
||||
if (address is ArtemisPeerAddress) {
|
||||
deployBridge(queueName, address.hostAndPort, nodeInfo.legalIdentity.name)
|
||||
} else {
|
||||
log.error("Don't know how to deal with $address for queue $queueName")
|
||||
}
|
||||
}
|
||||
if (!bridgeExists(address.queueName)) {
|
||||
deployBridge(address)
|
||||
|
||||
when {
|
||||
queueName.startsWith(PEERS_PREFIX) -> try {
|
||||
val identity = CompositeKey.parseFromBase58(queueName.substring(PEERS_PREFIX.length))
|
||||
val nodeInfo = networkMapCache.getNodeByLegalIdentityKey(identity)
|
||||
if (nodeInfo != null) {
|
||||
deployBridgeToPeer(nodeInfo)
|
||||
} else {
|
||||
log.error("Queue created for a peer that we don't know from the network map: $queueName")
|
||||
}
|
||||
} catch (e: AddressFormatException) {
|
||||
log.error("Flow violation: Could not parse peer queue name as Base 58: $queueName")
|
||||
}
|
||||
|
||||
queueName.startsWith(SERVICES_PREFIX) -> try {
|
||||
val identity = CompositeKey.parseFromBase58(queueName.substring(SERVICES_PREFIX.length))
|
||||
val nodeInfos = networkMapCache.getNodesByAdvertisedServiceIdentityKey(identity)
|
||||
// Create a bridge for each node advertising the service.
|
||||
for (nodeInfo in nodeInfos) {
|
||||
deployBridgeToPeer(nodeInfo)
|
||||
}
|
||||
} catch (e: AddressFormatException) {
|
||||
log.error("Flow violation: Could not parse service queue name as Base 58: $queueName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The bridge will be created automatically when the queues are created, however, this is not the case when the network map restarted.
|
||||
* The queues are restored from the journal, and because the queues are added before we register the callback handler, this method will never get called for existing queues.
|
||||
* This results in message queues up and never get send out. (https://github.com/corda/corda/issues/37)
|
||||
*
|
||||
* We create the bridges indirectly now because the network map is not persisted and there are no ways to obtain host and port information on startup.
|
||||
* TODO : Create the bridge directly from the list of queues on start up when we have a persisted network map service.
|
||||
*/
|
||||
private fun updateBridgesOnNetworkChange(change: MapChange) {
|
||||
fun gatherAddresses(node: NodeInfo): Sequence<ArtemisPeerAddress> {
|
||||
val peerAddress = node.address as ArtemisPeerAddress
|
||||
val addresses = mutableListOf(peerAddress)
|
||||
node.advertisedServices.mapTo(addresses) { NodeAddress.asService(it.identity.owningKey, peerAddress.hostAndPort) }
|
||||
return addresses.asSequence()
|
||||
}
|
||||
|
||||
fun deployBridges(node: NodeInfo) {
|
||||
gatherAddresses(node)
|
||||
.filter { queueExists(it.queueName) && !bridgeExists(it.bridgeName) }
|
||||
.forEach { deployBridge(it, node.legalIdentity.name) }
|
||||
}
|
||||
|
||||
fun destroyBridges(node: NodeInfo) {
|
||||
gatherAddresses(node).forEach {
|
||||
activeMQServer.destroyBridge(it.bridgeName)
|
||||
}
|
||||
}
|
||||
|
||||
when (change) {
|
||||
is MapChange.Added -> {
|
||||
deployBridges(change.node)
|
||||
}
|
||||
is MapChange.Removed -> {
|
||||
destroyBridges(change.node)
|
||||
}
|
||||
is MapChange.Modified -> {
|
||||
// TODO Figure out what has actually changed and only destroy those bridges that need to be.
|
||||
destroyBridges(change.previousNode)
|
||||
deployBridges(change.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deployBridge(address: ArtemisPeerAddress, legalName: String) {
|
||||
deployBridge(address.queueName, address.hostAndPort, legalName)
|
||||
}
|
||||
|
||||
/**
|
||||
* All nodes are expected to have a public facing address called [ArtemisMessagingComponent.P2P_QUEUE] for receiving
|
||||
* messages from other nodes. When we want to send a message to a node we send it to our internal address/queue for it,
|
||||
* as defined by ArtemisAddress.queueName. A bridge is then created to forward messages from this queue to the node's
|
||||
* P2P address.
|
||||
*/
|
||||
private fun deployBridge(address: ArtemisAddress) {
|
||||
private fun deployBridge(queueName: String, target: HostAndPort, legalName: String) {
|
||||
val tcpTransport = tcpTransport(Outbound(expectedCommonName = legalName), target.hostText, target.port)
|
||||
tcpTransport.params[ArtemisMessagingServer::class.java.name] = this
|
||||
// We intentionally overwrite any previous connector config in case the peer legal name changed
|
||||
activeMQServer.configuration.addConnectorConfiguration(target.toString(), tcpTransport)
|
||||
|
||||
activeMQServer.deployBridge(BridgeConfiguration().apply {
|
||||
name = address.queueName.toString()
|
||||
queueName = address.queueName.toString()
|
||||
name = getBridgeName(queueName, target)
|
||||
this.queueName = queueName
|
||||
forwardingAddress = P2P_QUEUE
|
||||
staticConnectors = listOf(address.hostAndPort.toString())
|
||||
staticConnectors = listOf(target.toString())
|
||||
confirmationWindowSize = 100000 // a guess
|
||||
isUseDuplicateDetection = true // Enable the bridge's automatic deduplication logic
|
||||
// We keep trying until the network map deems the node unreachable and tells us it's been removed at which
|
||||
// point we destroy the bridge
|
||||
// TODO Give some thought to the retry settings
|
||||
retryInterval = 5.seconds.toMillis()
|
||||
retryIntervalMultiplier = 1.5 // Exponential backoff
|
||||
maxRetryInterval = 3.minutes.toMillis()
|
||||
// As a peer of the target node we must connect to it using the peer user. Actual authentication is done using
|
||||
// our TLS certificate.
|
||||
user = PEER_USER
|
||||
@ -259,120 +351,201 @@ class ArtemisMessagingServer(override val config: NodeConfiguration,
|
||||
})
|
||||
}
|
||||
|
||||
private fun maybeDestroyBridge(name: SimpleString) {
|
||||
if (bridgeExists(name)) {
|
||||
activeMQServer.destroyBridge(name.toString())
|
||||
private fun queueExists(queueName: String): Boolean = activeMQServer.queueQuery(SimpleString(queueName)).isExists
|
||||
|
||||
private fun bridgeExists(bridgeName: String): Boolean = activeMQServer.clusterManager.bridges.containsKey(bridgeName)
|
||||
|
||||
private val ArtemisPeerAddress.bridgeName: String get() = getBridgeName(queueName, hostAndPort)
|
||||
|
||||
private fun getBridgeName(queueName: String, hostAndPort: HostAndPort): String = "$queueName -> $hostAndPort"
|
||||
|
||||
// This is called on one of Artemis' background threads
|
||||
internal fun hostVerificationFail(peerLegalName: String, expectedCommonName: String) {
|
||||
log.error("Peer has wrong CN - expected $expectedCommonName but got $peerLegalName. This is either a fatal " +
|
||||
"misconfiguration by the remote peer or an SSL man-in-the-middle attack!")
|
||||
if (expectedCommonName == config.networkMapService?.legalName) {
|
||||
// If the peer that failed host verification was the network map node then we're in big trouble and need to bail!
|
||||
_networkMapConnectionFuture!!.setException(IOException("${config.networkMapService} failed host verification check"))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
|
||||
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
|
||||
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
|
||||
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
|
||||
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
|
||||
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
|
||||
* the CN within that is their legal name.
|
||||
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
|
||||
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
|
||||
*/
|
||||
class NodeLoginModule : LoginModule {
|
||||
|
||||
companion object {
|
||||
// Include forbidden username character to prevent name clash with any RPC usernames
|
||||
const val PEER_ROLE = "SystemRoles/Peer"
|
||||
const val NODE_ROLE = "SystemRoles/Node"
|
||||
const val RPC_ROLE = "SystemRoles/RPC"
|
||||
}
|
||||
|
||||
private var loginSucceeded: Boolean = false
|
||||
private lateinit var subject: Subject
|
||||
private lateinit var callbackHandler: CallbackHandler
|
||||
private lateinit var userService: RPCUserService
|
||||
private lateinit var ourRootCAPublicKey: PublicKey
|
||||
private lateinit var ourPublicKey: PublicKey
|
||||
private val principals = ArrayList<Principal>()
|
||||
|
||||
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
|
||||
this.subject = subject
|
||||
this.callbackHandler = callbackHandler
|
||||
userService = options[RPCUserService::class.java.name] as RPCUserService
|
||||
ourRootCAPublicKey = options[CORDA_ROOT_CA] as PublicKey
|
||||
ourPublicKey = options[CORDA_CLIENT_CA] as PublicKey
|
||||
}
|
||||
|
||||
override fun login(): Boolean {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
val certificateCallback = CertificateCallback()
|
||||
|
||||
try {
|
||||
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
|
||||
} catch (e: IOException) {
|
||||
throw LoginException(e.message)
|
||||
} catch (e: UnsupportedCallbackException) {
|
||||
throw LoginException("${e.message} not available to obtain information from user")
|
||||
}
|
||||
|
||||
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
|
||||
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
|
||||
|
||||
val validatedUser = if (username == PEER_USER || username == NODE_USER) {
|
||||
val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?")
|
||||
val peerCertificate = certificates.first()
|
||||
val role = if (username == NODE_USER) {
|
||||
if (peerCertificate.publicKey != ourPublicKey) {
|
||||
throw FailedLoginException("Only the node can login as $NODE_USER")
|
||||
}
|
||||
NODE_ROLE
|
||||
} else {
|
||||
val theirRootCAPublicKey = certificates.last().publicKey
|
||||
if (theirRootCAPublicKey != ourRootCAPublicKey) {
|
||||
throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey")
|
||||
}
|
||||
PEER_ROLE // This enables the peer to send to our P2P address
|
||||
}
|
||||
principals += RolePrincipal(role)
|
||||
peerCertificate.subjectDN.name
|
||||
} else {
|
||||
// Otherwise assume they're an RPC user
|
||||
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
|
||||
if (password != rpcUser.password) {
|
||||
// TODO Switch to hashed passwords
|
||||
// TODO Retrieve client IP address to include in exception message
|
||||
throw FailedLoginException("Password for user $username does not match")
|
||||
}
|
||||
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
|
||||
principals += RolePrincipal("$CLIENTS_PREFIX$username") // This enables the RPC client to receive responses
|
||||
username
|
||||
}
|
||||
principals += UserPrincipal(validatedUser)
|
||||
|
||||
loginSucceeded = true
|
||||
return loginSucceeded
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
val result = loginSucceeded
|
||||
if (result) {
|
||||
subject.principals.addAll(principals)
|
||||
}
|
||||
clear()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun abort(): Boolean {
|
||||
clear()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun logout(): Boolean {
|
||||
subject.principals.removeAll(principals)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
loginSucceeded = false
|
||||
// This is called on one of Artemis' background threads
|
||||
internal fun onTcpConnection(peerLegalName: String) {
|
||||
if (peerLegalName == config.networkMapService?.legalName) {
|
||||
_networkMapConnectionFuture!!.set(Unit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VerifyingNettyConnectorFactory : NettyConnectorFactory() {
|
||||
override fun createConnector(configuration: MutableMap<String, Any>?,
|
||||
handler: BufferHandler?,
|
||||
listener: ClientConnectionLifeCycleListener?,
|
||||
closeExecutor: Executor?,
|
||||
threadPool: Executor?,
|
||||
scheduledThreadPool: ScheduledExecutorService?,
|
||||
protocolManager: ClientProtocolManager?): Connector {
|
||||
return VerifyingNettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool,
|
||||
protocolManager)
|
||||
}
|
||||
}
|
||||
|
||||
private class VerifyingNettyConnector(configuration: MutableMap<String, Any>?,
|
||||
handler: BufferHandler?,
|
||||
listener: ClientConnectionLifeCycleListener?,
|
||||
closeExecutor: Executor?,
|
||||
threadPool: Executor?,
|
||||
scheduledThreadPool: ScheduledExecutorService?,
|
||||
protocolManager: ClientProtocolManager?) :
|
||||
NettyConnector(configuration, handler, listener, closeExecutor, threadPool, scheduledThreadPool, protocolManager)
|
||||
{
|
||||
private val server = configuration?.get(ArtemisMessagingServer::class.java.name) as? ArtemisMessagingServer
|
||||
private val expectedCommonName = configuration?.get(ArtemisMessagingComponent.VERIFY_PEER_COMMON_NAME) as? String
|
||||
|
||||
override fun createConnection(): Connection? {
|
||||
val connection = super.createConnection() as NettyConnection?
|
||||
if (connection != null && expectedCommonName != null) {
|
||||
val peerLegalName = connection
|
||||
.channel
|
||||
.pipeline()
|
||||
.get(SslHandler::class.java)
|
||||
.engine()
|
||||
.session
|
||||
.peerPrincipal
|
||||
.name
|
||||
.let(::X500Name)
|
||||
.commonName
|
||||
// TODO Verify on the entire principle (subject)
|
||||
if (peerLegalName != expectedCommonName) {
|
||||
connection.close()
|
||||
server!!.hostVerificationFail(peerLegalName, expectedCommonName)
|
||||
return null // Artemis will keep trying to reconnect until it's told otherwise
|
||||
} else {
|
||||
server!!.onTcpConnection(peerLegalName)
|
||||
}
|
||||
}
|
||||
return connection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clients must connect to us with a username and password and must use TLS. If a someone connects with
|
||||
* [ArtemisMessagingComponent.NODE_USER] then we confirm it's just us as the node by checking their TLS certificate
|
||||
* is the same as our one in our key store. Then they're given full access to all valid queues. If they connect with
|
||||
* [ArtemisMessagingComponent.PEER_USER] then we confirm they belong on our P2P network by checking their root CA is
|
||||
* the same as our root CA. If that's the case the only access they're given is the ablility send to our P2P address.
|
||||
* In both cases the messages these authenticated nodes send to us are tagged with their subject DN and we assume
|
||||
* the CN within that is their legal name.
|
||||
* Otherwise if the username is neither of the above we assume it's an RPC user and authenticate against our list of
|
||||
* valid RPC users. RPC clients are given permission to perform RPC and nothing else.
|
||||
*/
|
||||
class NodeLoginModule : LoginModule {
|
||||
companion object {
|
||||
// Include forbidden username character to prevent name clash with any RPC usernames
|
||||
const val PEER_ROLE = "SystemRoles/Peer"
|
||||
const val NODE_ROLE = "SystemRoles/Node"
|
||||
const val RPC_ROLE = "SystemRoles/RPC"
|
||||
|
||||
val log = loggerFor<NodeLoginModule>()
|
||||
}
|
||||
|
||||
private var loginSucceeded: Boolean = false
|
||||
private lateinit var subject: Subject
|
||||
private lateinit var callbackHandler: CallbackHandler
|
||||
private lateinit var userService: RPCUserService
|
||||
private lateinit var ourRootCAPublicKey: PublicKey
|
||||
private lateinit var ourPublicKey: PublicKey
|
||||
private val principals = ArrayList<Principal>()
|
||||
|
||||
override fun initialize(subject: Subject, callbackHandler: CallbackHandler, sharedState: Map<String, *>, options: Map<String, *>) {
|
||||
this.subject = subject
|
||||
this.callbackHandler = callbackHandler
|
||||
userService = options[RPCUserService::class.java.name] as RPCUserService
|
||||
ourRootCAPublicKey = options[CORDA_ROOT_CA] as PublicKey
|
||||
ourPublicKey = options[CORDA_CLIENT_CA] as PublicKey
|
||||
}
|
||||
|
||||
override fun login(): Boolean {
|
||||
val nameCallback = NameCallback("Username: ")
|
||||
val passwordCallback = PasswordCallback("Password: ", false)
|
||||
val certificateCallback = CertificateCallback()
|
||||
|
||||
try {
|
||||
callbackHandler.handle(arrayOf(nameCallback, passwordCallback, certificateCallback))
|
||||
} catch (e: IOException) {
|
||||
throw LoginException(e.message)
|
||||
} catch (e: UnsupportedCallbackException) {
|
||||
throw LoginException("${e.message} not available to obtain information from user")
|
||||
}
|
||||
|
||||
val username = nameCallback.name ?: throw FailedLoginException("Username not provided")
|
||||
val password = String(passwordCallback.password ?: throw FailedLoginException("Password not provided"))
|
||||
|
||||
log.info("Processing login for $username")
|
||||
|
||||
val validatedUser = if (username == PEER_USER || username == NODE_USER) {
|
||||
val certificates = certificateCallback.certificates ?: throw FailedLoginException("No TLS?")
|
||||
authenticateNode(certificates, username)
|
||||
} else {
|
||||
// Otherwise assume they're an RPC user
|
||||
authenticateRpcUser(password, username)
|
||||
}
|
||||
principals += UserPrincipal(validatedUser)
|
||||
|
||||
loginSucceeded = true
|
||||
return loginSucceeded
|
||||
}
|
||||
|
||||
private fun authenticateNode(certificates: Array<X509Certificate>, username: String): String {
|
||||
val peerCertificate = certificates.first()
|
||||
val role = if (username == NODE_USER) {
|
||||
if (peerCertificate.publicKey != ourPublicKey) {
|
||||
throw FailedLoginException("Only the node can login as $NODE_USER")
|
||||
}
|
||||
NODE_ROLE
|
||||
} else {
|
||||
val theirRootCAPublicKey = certificates.last().publicKey
|
||||
if (theirRootCAPublicKey != ourRootCAPublicKey) {
|
||||
throw FailedLoginException("Peer does not belong on our network. Their root CA: $theirRootCAPublicKey")
|
||||
}
|
||||
PEER_ROLE // This enables the peer to send to our P2P address
|
||||
}
|
||||
principals += RolePrincipal(role)
|
||||
return peerCertificate.subjectDN.name
|
||||
}
|
||||
|
||||
private fun authenticateRpcUser(password: String, username: String): String {
|
||||
val rpcUser = userService.getUser(username) ?: throw FailedLoginException("User does not exist")
|
||||
if (password != rpcUser.password) {
|
||||
// TODO Switch to hashed passwords
|
||||
// TODO Retrieve client IP address to include in exception message
|
||||
throw FailedLoginException("Password for user $username does not match")
|
||||
}
|
||||
principals += RolePrincipal(RPC_ROLE) // This enables the RPC client to send requests
|
||||
principals += RolePrincipal("$CLIENTS_PREFIX$username") // This enables the RPC client to receive responses
|
||||
return username
|
||||
}
|
||||
|
||||
override fun commit(): Boolean {
|
||||
val result = loginSucceeded
|
||||
if (result) {
|
||||
subject.principals.addAll(principals)
|
||||
}
|
||||
clear()
|
||||
return result
|
||||
}
|
||||
|
||||
override fun abort(): Boolean {
|
||||
clear()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun logout(): Boolean {
|
||||
subject.principals.removeAll(principals)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun clear() {
|
||||
loginSucceeded = false
|
||||
}
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ package net.corda.node.services.messaging
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.logElapsedTime
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQException
|
||||
import org.apache.activemq.artemis.api.core.client.ActiveMQClient
|
||||
import org.apache.activemq.artemis.api.core.client.ClientSession
|
||||
@ -16,9 +19,16 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
/**
|
||||
* An RPC client connects to the specified server and allows you to make calls to the server that perform various
|
||||
* useful tasks. See the documentation for [proxy] or review the docsite to learn more about how this API works.
|
||||
*
|
||||
* @param host The hostname and messaging port of the node.
|
||||
* @param config If specified, the SSL configuration to use. If not specified, SSL will be disabled and the node will not be authenticated, nor will RPC traffic be encrypted.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfiguration) : Closeable, ArtemisMessagingComponent() {
|
||||
class CordaRPCClient(val host: HostAndPort, override val config: SSLConfiguration?) : Closeable, ArtemisMessagingComponent() {
|
||||
private companion object {
|
||||
val log = loggerFor<CordaRPCClient>()
|
||||
}
|
||||
|
||||
// TODO: Certificate handling for clients needs more work.
|
||||
private inner class State {
|
||||
var running = false
|
||||
@ -29,30 +39,54 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
|
||||
|
||||
private val state = ThreadBox(State())
|
||||
|
||||
/** Opens the connection to the server and registers a JVM shutdown hook to cleanly disconnect. */
|
||||
/**
|
||||
* Opens the connection to the server with the given username and password, then returns itself.
|
||||
* Registers a JVM shutdown hook to cleanly disconnect.
|
||||
*/
|
||||
@Throws(ActiveMQException::class)
|
||||
fun start(username: String, password: String) {
|
||||
fun start(username: String, password: String): CordaRPCClient {
|
||||
state.locked {
|
||||
check(!running)
|
||||
checkStorePasswords()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(ConnectionDirection.OUTBOUND, host.hostText, host.port))
|
||||
serverLocator.threadPoolMaxSize = 1
|
||||
// TODO: Configure session reconnection, confirmation window sizes and other Artemis features.
|
||||
// This will allow reconnection in case of server restart/network outages/IP address changes, etc.
|
||||
// See http://activemq.apache.org/artemis/docs/1.5.0/client-reconnection.html
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
|
||||
session.start()
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, username)
|
||||
running = true
|
||||
log.logElapsedTime("Startup") {
|
||||
checkStorePasswords()
|
||||
val serverLocator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport(Outbound(), host.hostText, host.port))
|
||||
serverLocator.threadPoolMaxSize = 1
|
||||
// TODO: Configure session reconnection, confirmation window sizes and other Artemis features.
|
||||
// This will allow reconnection in case of server restart/network outages/IP address changes, etc.
|
||||
// See http://activemq.apache.org/artemis/docs/1.5.0/client-reconnection.html
|
||||
sessionFactory = serverLocator.createSessionFactory()
|
||||
session = sessionFactory.createSession(username, password, false, true, true, serverLocator.isPreAcknowledge, serverLocator.ackBatchSize)
|
||||
session.start()
|
||||
clientImpl = CordaRPCClientImpl(session, state.lock, username)
|
||||
running = true
|
||||
}
|
||||
}
|
||||
|
||||
Runtime.getRuntime().addShutdownHook(Thread {
|
||||
close()
|
||||
})
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
/** Shuts down the client and lets the server know it can free the used resources (in a nice way) */
|
||||
/**
|
||||
* A convenience function that opens a connection with the given credentials, executes the given code block with all
|
||||
* available RPCs in scope and shuts down the RPC connection again. It's meant for quick prototyping and demos. For
|
||||
* more control you probably want to control the lifecycle of the client and proxies independently, as well as
|
||||
* configuring a timeout and other such features via the [proxy] method.
|
||||
*
|
||||
* After this method returns the client is closed and can't be restarted.
|
||||
*/
|
||||
@Throws(ActiveMQException::class)
|
||||
fun <T> use(username: String, password: String, block: CordaRPCOps.() -> T): T {
|
||||
require(!state.locked { running })
|
||||
start(username, password)
|
||||
(this as Closeable).use {
|
||||
return proxy().block()
|
||||
}
|
||||
}
|
||||
|
||||
/** Shuts down the client and lets the server know it can free the used resources (in a nice way). */
|
||||
override fun close() {
|
||||
state.locked {
|
||||
if (!running) return
|
||||
@ -96,17 +130,18 @@ class CordaRPCClient(val host: HostAndPort, override val config: NodeSSLConfigur
|
||||
*
|
||||
* @throws RPCException if the server version is too low or if the server isn't reachable within the given time.
|
||||
*/
|
||||
|
||||
// TODO: Add an @JvmOverloads annotation
|
||||
|
||||
@JvmOverloads
|
||||
@Throws(RPCException::class)
|
||||
fun proxy(timeout: Duration? = null, minVersion: Int = 0): CordaRPCOps {
|
||||
return state.locked {
|
||||
check(running) { "Client must have been started first" }
|
||||
clientImpl.proxyFor(CordaRPCOps::class.java, timeout, minVersion)
|
||||
log.logElapsedTime("Proxy build") {
|
||||
clientImpl.proxyFor(CordaRPCOps::class.java, timeout, minVersion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
private fun finalize() {
|
||||
state.locked {
|
||||
if (running) {
|
||||
|
@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.messaging.*
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.opaque
|
||||
import net.corda.core.success
|
||||
@ -13,6 +14,7 @@ import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.api.MessagingServiceInternal
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.ConnectionDirection.Outbound
|
||||
import net.corda.node.utilities.*
|
||||
import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException
|
||||
import org.apache.activemq.artemis.api.core.Message.HDR_DUPLICATE_DETECTION_ID
|
||||
@ -47,13 +49,13 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
* @param serverHostPort The address of the broker instance to connect to (might be running in the same process)
|
||||
* @param myIdentity Either the public key to be used as the ArtemisMQ address and queue name for the node globally, or null to indicate
|
||||
* that this is a NetworkMapService node which will be bound globally to the name "networkmap"
|
||||
* @param executor An executor to run received message tasks upon.
|
||||
* @param nodeExecutor An executor to run received message tasks upon.
|
||||
*/
|
||||
@ThreadSafe
|
||||
class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
val serverHostPort: HostAndPort,
|
||||
val myIdentity: CompositeKey?,
|
||||
val executor: AffinityExecutor,
|
||||
val nodeExecutor: AffinityExecutor,
|
||||
val database: Database,
|
||||
val networkMapRegistrationFuture: ListenableFuture<Unit>) : ArtemisMessagingComponent(), MessagingServiceInternal {
|
||||
companion object {
|
||||
@ -65,15 +67,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
// confusion.
|
||||
const val TOPIC_PROPERTY = "platform-topic"
|
||||
const val SESSION_ID_PROPERTY = "session-id"
|
||||
|
||||
const val RPC_QUEUE_REMOVALS_QUEUE = "rpc.qremovals"
|
||||
|
||||
/**
|
||||
* This should be the only way to generate an ArtemisAddress and that only of the remote NetworkMapService node.
|
||||
* All other addresses come from the NetworkMapCache, or myAddress below.
|
||||
* The node will populate with their own identity based address when they register with the NetworkMapService.
|
||||
*/
|
||||
fun makeNetworkMapAddress(hostAndPort: HostAndPort): SingleMessageRecipient = NetworkMapAddress(hostAndPort)
|
||||
}
|
||||
|
||||
private class InnerState {
|
||||
@ -93,10 +86,13 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
data class Handler(val topicSession: TopicSession,
|
||||
val callback: (ReceivedMessage, MessageHandlerRegistration) -> Unit) : MessageHandlerRegistration
|
||||
|
||||
/** An executor for sending messages */
|
||||
private val messagingExecutor = AffinityExecutor.ServiceAffinityExecutor("${config.myLegalName} Messaging", 1)
|
||||
|
||||
/**
|
||||
* Apart from the NetworkMapService this is the only other address accessible to the node outside of lookups against the NetworkMapCache.
|
||||
*/
|
||||
override val myAddress: SingleMessageRecipient = if (myIdentity != null) NodeAddress(myIdentity, serverHostPort) else NetworkMapAddress(serverHostPort)
|
||||
override val myAddress: SingleMessageRecipient = if (myIdentity != null) NodeAddress.asPeer(myIdentity, serverHostPort) else NetworkMapAddress(serverHostPort)
|
||||
|
||||
private val state = ThreadBox(InnerState())
|
||||
private val handlers = CopyOnWriteArrayList<Handler>()
|
||||
@ -119,7 +115,8 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
started = true
|
||||
|
||||
log.info("Connecting to server: $serverHostPort")
|
||||
val tcpTransport = tcpTransport(ConnectionDirection.OUTBOUND, serverHostPort.hostText, serverHostPort.port)
|
||||
// TODO Add broker CN to config for host verification in case the embedded broker isn't used
|
||||
val tcpTransport = tcpTransport(Outbound(), serverHostPort.hostText, serverHostPort.port)
|
||||
val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport)
|
||||
clientFactory = locator.createSessionFactory()
|
||||
|
||||
@ -135,7 +132,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
producer = session.createProducer()
|
||||
|
||||
// Create a queue, consumer and producer for handling P2P network messages.
|
||||
createQueueIfAbsent(SimpleString(P2P_QUEUE))
|
||||
p2pConsumer = makeP2PConsumer(session, true)
|
||||
networkMapRegistrationFuture.success {
|
||||
state.locked {
|
||||
@ -149,13 +145,6 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
// Create an RPC queue and consumer: this will service locally connected clients only (not via a
|
||||
// bridge) and those clients must have authenticated. We could use a single consumer for everything
|
||||
// and perhaps we should, but these queues are not worth persisting.
|
||||
session.createTemporaryQueue(RPC_REQUESTS_QUEUE, RPC_REQUESTS_QUEUE)
|
||||
// The custom name for the queue is intentional - we may wish other things to subscribe to the
|
||||
// NOTIFICATIONS_ADDRESS with different filters in future
|
||||
session.createTemporaryQueue(NOTIFICATIONS_ADDRESS, RPC_QUEUE_REMOVALS_QUEUE, "_AMQ_NotifType = 1")
|
||||
rpcConsumer = session.createConsumer(RPC_REQUESTS_QUEUE)
|
||||
rpcNotificationConsumer = session.createConsumer(RPC_QUEUE_REMOVALS_QUEUE)
|
||||
rpcDispatcher = createRPCDispatcher(rpcOps, userService, config.myLegalName)
|
||||
@ -219,7 +208,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
check(started) { "start must be called first" }
|
||||
check(!running) { "run can't be called twice" }
|
||||
running = true
|
||||
rpcDispatcher!!.start(rpcConsumer!!, rpcNotificationConsumer!!, executor)
|
||||
rpcDispatcher!!.start(rpcConsumer!!, rpcNotificationConsumer!!, nodeExecutor)
|
||||
p2pConsumer!!
|
||||
}
|
||||
|
||||
@ -301,7 +290,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
//
|
||||
// Note that handlers may re-enter this class. We aren't holding any locks and methods like
|
||||
// start/run/stop have re-entrancy assertions at the top, so it is OK.
|
||||
executor.fetchFrom {
|
||||
nodeExecutor.fetchFrom {
|
||||
databaseTransaction(database) {
|
||||
if (msg.uniqueMessageId in processedMessages) {
|
||||
log.trace { "Discard duplicate message ${msg.uniqueMessageId} for ${msg.topicSession}" }
|
||||
@ -344,7 +333,7 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
p2pConsumer = null
|
||||
prevRunning
|
||||
}
|
||||
if (running && !executor.isOnThread) {
|
||||
if (running && !nodeExecutor.isOnThread) {
|
||||
// Wait for the main loop to notice the consumer has gone and finish up.
|
||||
shutdownLatch.await()
|
||||
}
|
||||
@ -367,26 +356,30 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
}
|
||||
|
||||
override fun send(message: Message, target: MessageRecipients) {
|
||||
state.locked {
|
||||
val mqAddress = getMQAddress(target)
|
||||
val artemisMessage = session!!.createMessage(true).apply {
|
||||
val sessionID = message.topicSession.sessionID
|
||||
putStringProperty(TOPIC_PROPERTY, message.topicSession.topic)
|
||||
putLongProperty(SESSION_ID_PROPERTY, sessionID)
|
||||
writeBodyBufferBytes(message.data)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(message.uniqueMessageId.toString()))
|
||||
// We have to perform sending on a different thread pool, since using the same pool for messaging and
|
||||
// fibers leads to Netty buffer memory leaks, caused by both Netty and Quasar fiddling with thread-locals.
|
||||
messagingExecutor.fetchFrom {
|
||||
state.locked {
|
||||
val mqAddress = getMQAddress(target)
|
||||
val artemisMessage = session!!.createMessage(true).apply {
|
||||
val sessionID = message.topicSession.sessionID
|
||||
putStringProperty(TOPIC_PROPERTY, message.topicSession.topic)
|
||||
putLongProperty(SESSION_ID_PROPERTY, sessionID)
|
||||
writeBodyBufferBytes(message.data)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(message.uniqueMessageId.toString()))
|
||||
}
|
||||
log.info("Send to: $mqAddress topic: ${message.topicSession.topic} sessionID: ${message.topicSession.sessionID} " +
|
||||
"uuid: ${message.uniqueMessageId}")
|
||||
producer!!.send(mqAddress, artemisMessage)
|
||||
}
|
||||
|
||||
log.info("Send to: $mqAddress topic: ${message.topicSession.topic} sessionID: ${message.topicSession.sessionID} uuid: ${message.uniqueMessageId}")
|
||||
producer!!.send(mqAddress, artemisMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMQAddress(target: MessageRecipients): SimpleString {
|
||||
private fun getMQAddress(target: MessageRecipients): String {
|
||||
return if (target == myAddress) {
|
||||
// If we are sending to ourselves then route the message directly to our P2P queue.
|
||||
SimpleString(P2P_QUEUE)
|
||||
P2P_QUEUE
|
||||
} else {
|
||||
// Otherwise we send the message to an internal queue for the target residing on our broker. It's then the
|
||||
// broker's job to route the message to the target's P2P queue.
|
||||
@ -397,9 +390,9 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
}
|
||||
|
||||
/** Attempts to create a durable queue on the broker which is bound to an address of the same name. */
|
||||
private fun createQueueIfAbsent(queueName: SimpleString) {
|
||||
private fun createQueueIfAbsent(queueName: String) {
|
||||
state.alreadyLocked {
|
||||
val queueQuery = session!!.queueQuery(queueName)
|
||||
val queueQuery = session!!.queueQuery(SimpleString(queueName))
|
||||
if (!queueQuery.isExists) {
|
||||
log.info("Create fresh queue $queueName bound on same address")
|
||||
session!!.createQueue(queueName, queueName, true)
|
||||
@ -439,14 +432,23 @@ class NodeMessagingClient(override val config: NodeConfiguration,
|
||||
private fun createRPCDispatcher(ops: RPCOps, userService: RPCUserService, nodeLegalName: String) =
|
||||
object : RPCDispatcher(ops, userService, nodeLegalName) {
|
||||
override fun send(data: SerializedBytes<*>, toAddress: String) {
|
||||
state.locked {
|
||||
val msg = session!!.createMessage(false).apply {
|
||||
writeBodyBufferBytes(data.bytes)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
messagingExecutor.fetchFrom {
|
||||
state.locked {
|
||||
val msg = session!!.createMessage(false).apply {
|
||||
writeBodyBufferBytes(data.bytes)
|
||||
// Use the magic deduplication property built into Artemis as our message identity too
|
||||
putStringProperty(HDR_DUPLICATE_DETECTION_ID, SimpleString(UUID.randomUUID().toString()))
|
||||
}
|
||||
producer!!.send(toAddress, msg)
|
||||
}
|
||||
producer!!.send(toAddress, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAddressOfParty(partyInfo: PartyInfo): MessageRecipients {
|
||||
return when (partyInfo) {
|
||||
is PartyInfo.Node -> partyInfo.node.address
|
||||
is PartyInfo.Service -> ArtemisMessagingComponent.ServiceAddress(partyInfo.service.identity.owningKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ import net.corda.flows.CashFlowResult
|
||||
import net.corda.node.internal.AbstractNode
|
||||
import net.corda.node.services.User
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.Companion.NODE_USER
|
||||
import net.corda.node.services.messaging.ArtemisMessagingComponent.NetworkMapAddress
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
@ -41,9 +42,7 @@ import org.slf4j.LoggerFactory
|
||||
import rx.Notification
|
||||
import rx.Observable
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
/** Global RPC logger */
|
||||
@ -146,6 +145,7 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
ImmutableMultimapSerializer.registerSerializers(this)
|
||||
|
||||
register(BufferedInputStream::class.java, InputStreamSerializer)
|
||||
register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer)
|
||||
|
||||
noReferencesWithin<WireTransaction>()
|
||||
|
||||
@ -198,18 +198,8 @@ private class RPCKryo(observableSerializer: Serializer<Observable<Any>>? = null)
|
||||
register(NetworkMapCache.MapChange.Added::class.java)
|
||||
register(NetworkMapCache.MapChange.Removed::class.java)
|
||||
register(NetworkMapCache.MapChange.Modified::class.java)
|
||||
register(ArtemisMessagingComponent.NodeAddress::class.java,
|
||||
read = { kryo, input ->
|
||||
ArtemisMessagingComponent.NodeAddress(
|
||||
CompositeKey.parseFromBase58(kryo.readObject(input, String::class.java)),
|
||||
kryo.readObject(input, HostAndPort::class.java))
|
||||
},
|
||||
write = { kryo, output, nodeAddress ->
|
||||
kryo.writeObject(output, nodeAddress.identity.toBase58String())
|
||||
kryo.writeObject(output, nodeAddress.hostAndPort)
|
||||
}
|
||||
)
|
||||
register(NodeMessagingClient.makeNetworkMapAddress(HostAndPort.fromString("localhost:0")).javaClass)
|
||||
register(ArtemisMessagingComponent.NodeAddress::class.java)
|
||||
register(NetworkMapAddress::class.java)
|
||||
register(ServiceInfo::class.java)
|
||||
register(ServiceType.getServiceType("ab", "ab").javaClass)
|
||||
register(ServiceType.parse("ab").javaClass)
|
||||
|
@ -14,6 +14,7 @@ import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import net.corda.core.node.services.NetworkCacheError
|
||||
import net.corda.core.node.services.NetworkMapCache
|
||||
import net.corda.core.node.services.NetworkMapCache.MapChange
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
@ -25,6 +26,7 @@ import net.corda.node.services.network.NetworkMapService.FetchMapResponse
|
||||
import net.corda.node.services.network.NetworkMapService.SubscribeResponse
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||
import net.corda.node.utilities.wrapWithDatabaseTransaction
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.security.SignatureException
|
||||
@ -43,7 +45,8 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
override val partyNodes: List<NodeInfo> get() = registeredNodes.map { it.value }
|
||||
override val networkMapNodes: List<NodeInfo> get() = getNodesWithService(NetworkMapService.type)
|
||||
private val _changed = PublishSubject.create<MapChange>()
|
||||
override val changed: Observable<MapChange> get() = _changed
|
||||
// We use assignment here so that multiple subscribers share the same wrapped Observable.
|
||||
override val changed: Observable<MapChange> = _changed.wrapWithDatabaseTransaction()
|
||||
private val changePublisher: rx.Observer<MapChange> get() = _changed.bufferUntilDatabaseCommit()
|
||||
|
||||
private val _registrationFuture = SettableFuture.create<Unit>()
|
||||
@ -52,9 +55,24 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach
|
||||
private var registeredForPush = false
|
||||
protected var registeredNodes: MutableMap<Party, NodeInfo> = Collections.synchronizedMap(HashMap<Party, NodeInfo>())
|
||||
|
||||
override fun getPartyInfo(party: Party): PartyInfo? {
|
||||
val node = registeredNodes[party]
|
||||
if (node != null) {
|
||||
return PartyInfo.Node(node)
|
||||
}
|
||||
for (entry in registeredNodes) {
|
||||
for (service in entry.value.advertisedServices) {
|
||||
if (service.identity == party) {
|
||||
return PartyInfo.Service(service)
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun track(): Pair<List<NodeInfo>, Observable<MapChange>> {
|
||||
synchronized(_changed) {
|
||||
return Pair(partyNodes, _changed.bufferUntilSubscribed())
|
||||
return Pair(partyNodes, _changed.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,7 +58,7 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag
|
||||
mutex.locked {
|
||||
return Pair(
|
||||
stateMachineTransactionMap.map { StateMachineTransactionMapping(it.value, it.key) },
|
||||
updates.bufferUntilSubscribed()
|
||||
updates.bufferUntilSubscribed().wrapWithDatabaseTransaction()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -59,12 +59,11 @@ class DBTransactionStorage : TransactionStorage {
|
||||
}
|
||||
|
||||
val updatesPublisher = PublishSubject.create<SignedTransaction>().toSerialized()
|
||||
override val updates: Observable<SignedTransaction>
|
||||
get() = updatesPublisher
|
||||
override val updates: Observable<SignedTransaction> = updatesPublisher.wrapWithDatabaseTransaction()
|
||||
|
||||
override fun track(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> {
|
||||
synchronized(txStorage) {
|
||||
return Pair(txStorage.values.toList(), updates.bufferUntilSubscribed())
|
||||
return Pair(txStorage.values.toList(), updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package net.corda.node.services.statemachine
|
||||
|
||||
import net.corda.node.services.statemachine.StateMachineManager.FlowSession
|
||||
import net.corda.node.services.statemachine.StateMachineManager.SessionMessage
|
||||
|
||||
// TODO revisit when Kotlin 1.1 is released and data classes can extend other classes
|
||||
interface FlowIORequest {
|
@ -7,18 +7,18 @@ import co.paralleluniverse.strands.Strand
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSessionException
|
||||
import net.corda.core.flows.FlowStateMachine
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.services.statemachine.StateMachineManager.*
|
||||
import net.corda.node.services.statemachine.StateMachineManager.FlowSession
|
||||
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState
|
||||
import net.corda.node.utilities.StrandLocalTransactionManager
|
||||
import net.corda.node.utilities.createDatabaseTransaction
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.Transaction
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
@ -31,7 +31,6 @@ import java.util.concurrent.ExecutionException
|
||||
class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
val logic: FlowLogic<R>,
|
||||
scheduler: FiberScheduler) : Fiber<R>("flow", scheduler), FlowStateMachine<R> {
|
||||
|
||||
companion object {
|
||||
// Used to work around a small limitation in Quasar.
|
||||
private val QUASAR_UNBLOCKER = run {
|
||||
@ -47,12 +46,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
// These fields shouldn't be serialised, so they are marked @Transient.
|
||||
@Transient lateinit override var serviceHub: ServiceHubInternal
|
||||
@Transient override lateinit var serviceHub: ServiceHubInternal
|
||||
@Transient internal lateinit var database: Database
|
||||
@Transient internal lateinit var actionOnSuspend: (FlowIORequest) -> Unit
|
||||
@Transient internal lateinit var actionOnEnd: () -> Unit
|
||||
@Transient internal lateinit var database: Database
|
||||
@Transient internal var fromCheckpoint: Boolean = false
|
||||
@Transient internal var txTrampoline: Transaction? = null
|
||||
@Transient private var txTrampoline: Transaction? = null
|
||||
|
||||
@Transient private var _logger: Logger? = null
|
||||
override val logger: Logger get() {
|
||||
@ -76,7 +75,7 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
internal val openSessions = HashMap<Pair<FlowLogic<*>, Party>, FlowSession>()
|
||||
|
||||
init {
|
||||
logic.fsm = this
|
||||
logic.stateMachine = this
|
||||
name = id.toString()
|
||||
}
|
||||
|
||||
@ -87,14 +86,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
logic.call()
|
||||
} catch (t: Throwable) {
|
||||
actionOnEnd()
|
||||
commitTransaction()
|
||||
_resultFuture?.setException(t)
|
||||
throw ExecutionException(t)
|
||||
}
|
||||
|
||||
// This is to prevent actionOnEnd being called twice if it throws an exception
|
||||
actionOnEnd()
|
||||
commitTransaction()
|
||||
_resultFuture?.set(result)
|
||||
return result
|
||||
}
|
||||
@ -120,9 +117,9 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun <T : Any> sendAndReceive(otherParty: Party,
|
||||
override fun <T : Any> sendAndReceive(receiveType: Class<T>,
|
||||
otherParty: Party,
|
||||
payload: Any,
|
||||
receiveType: Class<T>,
|
||||
sessionFlow: FlowLogic<*>): UntrustworthyData<T> {
|
||||
val (session, new) = getSession(otherParty, sessionFlow, payload)
|
||||
val receivedSessionData = if (new) {
|
||||
@ -132,16 +129,15 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
val sendSessionData = createSessionData(session, payload)
|
||||
sendAndReceiveInternal<SessionData>(session, sendSessionData)
|
||||
}
|
||||
return UntrustworthyData(receiveType.cast(receivedSessionData.payload))
|
||||
return receivedSessionData.checkPayloadIs(receiveType)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun <T : Any> receive(otherParty: Party,
|
||||
receiveType: Class<T>,
|
||||
override fun <T : Any> receive(receiveType: Class<T>,
|
||||
otherParty: Party,
|
||||
sessionFlow: FlowLogic<*>): UntrustworthyData<T> {
|
||||
val session = getSession(otherParty, sessionFlow, null).first
|
||||
val receivedSessionData = receiveInternal<SessionData>(session)
|
||||
return UntrustworthyData(receiveType.cast(receivedSessionData.payload))
|
||||
return receiveInternal<SessionData>(session).checkPayloadIs(receiveType)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -154,9 +150,12 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
}
|
||||
|
||||
private fun createSessionData(session: FlowSession, payload: Any): SessionData {
|
||||
val otherPartySessionId = session.otherPartySessionId
|
||||
?: throw IllegalStateException("We've somehow held onto an unconfirmed session: $session")
|
||||
return SessionData(otherPartySessionId, payload)
|
||||
val sessionState = session.state
|
||||
val peerSessionId = when (sessionState) {
|
||||
is FlowSessionState.Initiating -> throw IllegalStateException("We've somehow held onto an unconfirmed session: $session")
|
||||
is FlowSessionState.Initiated -> sessionState.peerSessionId
|
||||
}
|
||||
return SessionData(peerSessionId, payload)
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -164,12 +163,13 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
suspend(SendOnly(session, message))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private inline fun <reified M : SessionMessage> receiveInternal(session: FlowSession): M {
|
||||
private inline fun <reified M : ExistingSessionMessage> receiveInternal(session: FlowSession): ReceivedSessionMessage<M> {
|
||||
return suspendAndExpectReceive(ReceiveOnly(session, M::class.java))
|
||||
}
|
||||
|
||||
private inline fun <reified M : SessionMessage> sendAndReceiveInternal(session: FlowSession, message: SessionMessage): M {
|
||||
private inline fun <reified M : ExistingSessionMessage> sendAndReceiveInternal(
|
||||
session: FlowSession,
|
||||
message: SessionMessage): ReceivedSessionMessage<M> {
|
||||
return suspendAndExpectReceive(SendAndReceive(session, message, M::class.java))
|
||||
}
|
||||
|
||||
@ -191,26 +191,26 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
*/
|
||||
@Suspendable
|
||||
private fun startNewSession(otherParty: Party, sessionFlow: FlowLogic<*>, firstPayload: Any?): FlowSession {
|
||||
val node = serviceHub.networkMapCache.getRepresentativeNode(otherParty) ?: throw IllegalArgumentException("Don't know about party $otherParty")
|
||||
val nodeIdentity = node.legalIdentity
|
||||
logger.trace { "Initiating a new session with $nodeIdentity (representative of $otherParty)" }
|
||||
val session = FlowSession(sessionFlow, nodeIdentity, random63BitValue(), null)
|
||||
openSessions[Pair(sessionFlow, nodeIdentity)] = session
|
||||
val counterpartyFlow = sessionFlow.getCounterpartyMarker(nodeIdentity).name
|
||||
logger.trace { "Initiating a new session with $otherParty" }
|
||||
val session = FlowSession(sessionFlow, random63BitValue(), FlowSessionState.Initiating(otherParty))
|
||||
openSessions[Pair(sessionFlow, otherParty)] = session
|
||||
val counterpartyFlow = sessionFlow.getCounterpartyMarker(otherParty).name
|
||||
val sessionInit = SessionInit(session.ourSessionId, counterpartyFlow, firstPayload)
|
||||
val sessionInitResponse = sendAndReceiveInternal<SessionInitResponse>(session, sessionInit)
|
||||
val (peerParty, sessionInitResponse) = sendAndReceiveInternal<SessionInitResponse>(session, sessionInit)
|
||||
if (sessionInitResponse is SessionConfirm) {
|
||||
session.otherPartySessionId = sessionInitResponse.initiatedSessionId
|
||||
require(session.state is FlowSessionState.Initiating)
|
||||
session.state = FlowSessionState.Initiated(peerParty, sessionInitResponse.initiatedSessionId)
|
||||
return session
|
||||
} else {
|
||||
sessionInitResponse as SessionReject
|
||||
throw FlowSessionException("Party $nodeIdentity rejected session attempt: ${sessionInitResponse.errorMessage}")
|
||||
throw FlowException("Party $otherParty rejected session request: ${sessionInitResponse.errorMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun <M : SessionMessage> suspendAndExpectReceive(receiveRequest: ReceiveRequest<M>): M {
|
||||
fun getReceivedMessage(): ExistingSessionMessage? = receiveRequest.session.receivedMessages.poll()
|
||||
private fun <M : ExistingSessionMessage> suspendAndExpectReceive(receiveRequest: ReceiveRequest<M>): ReceivedSessionMessage<M> {
|
||||
val session = receiveRequest.session
|
||||
fun getReceivedMessage(): ReceivedSessionMessage<ExistingSessionMessage>? = session.receivedMessages.poll()
|
||||
|
||||
val polledMessage = getReceivedMessage()
|
||||
val receivedMessage = if (polledMessage != null) {
|
||||
@ -222,17 +222,21 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
} else {
|
||||
// Suspend while we wait for a receive
|
||||
suspend(receiveRequest)
|
||||
getReceivedMessage()
|
||||
?: throw IllegalStateException("Was expecting a ${receiveRequest.receiveType.simpleName} but got nothing: $receiveRequest")
|
||||
getReceivedMessage() ?:
|
||||
throw IllegalStateException("Was expecting a ${receiveRequest.receiveType.simpleName} but instead " +
|
||||
"got nothing: $receiveRequest")
|
||||
}
|
||||
|
||||
if (receivedMessage is SessionEnd) {
|
||||
openSessions.values.remove(receiveRequest.session)
|
||||
throw FlowSessionException("Counterparty on ${receiveRequest.session.otherParty} has prematurely ended on $receiveRequest")
|
||||
} else if (receiveRequest.receiveType.isInstance(receivedMessage)) {
|
||||
return receiveRequest.receiveType.cast(receivedMessage)
|
||||
if (receivedMessage.message is SessionEnd) {
|
||||
openSessions.values.remove(session)
|
||||
throw FlowException("Party ${session.state.sendToParty} has ended their flow but we were expecting to " +
|
||||
"receive ${receiveRequest.receiveType.simpleName} from them")
|
||||
} else if (receiveRequest.receiveType.isInstance(receivedMessage.message)) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return receivedMessage as ReceivedSessionMessage<M>
|
||||
} else {
|
||||
throw IllegalStateException("Was expecting a ${receiveRequest.receiveType.simpleName} but got $receivedMessage: $receiveRequest")
|
||||
throw IllegalStateException("Was expecting a ${receiveRequest.receiveType.simpleName} but instead got " +
|
||||
"${receivedMessage.message}: $receiveRequest")
|
||||
}
|
||||
}
|
||||
|
||||
@ -242,31 +246,28 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
txTrampoline = TransactionManager.currentOrNull()
|
||||
StrandLocalTransactionManager.setThreadLocalTx(null)
|
||||
ioRequest.session.waitingForResponse = (ioRequest is ReceiveRequest<*>)
|
||||
|
||||
var exceptionDuringSuspend: Throwable? = null
|
||||
parkAndSerialize { fiber, serializer ->
|
||||
logger.trace { "Suspended on $ioRequest" }
|
||||
// restore the Tx onto the ThreadLocal so that we can commit the ensuing checkpoint to the DB
|
||||
StrandLocalTransactionManager.setThreadLocalTx(txTrampoline)
|
||||
txTrampoline = null
|
||||
try {
|
||||
StrandLocalTransactionManager.setThreadLocalTx(txTrampoline)
|
||||
txTrampoline = null
|
||||
actionOnSuspend(ioRequest)
|
||||
} catch (t: Throwable) {
|
||||
// Do not throw exception again - Quasar completely bins it.
|
||||
logger.warn("Captured exception which was swallowed by Quasar for $logic at ${fiber.stackTrace.toList().joinToString("\n")}", t)
|
||||
// TODO When error handling is introduced, look into whether we should be deleting the checkpoint and
|
||||
// completing the Future
|
||||
processException(t)
|
||||
// Quasar does not terminate the fiber properly if an exception occurs during a suspend. We have to
|
||||
// resume the fiber just so that we can throw it when it's running.
|
||||
exceptionDuringSuspend = t
|
||||
resume(scheduler)
|
||||
}
|
||||
}
|
||||
logger.trace { "Resumed from $ioRequest" }
|
||||
createTransaction()
|
||||
}
|
||||
|
||||
private fun processException(t: Throwable) {
|
||||
// This can get called in actionOnSuspend *after* we commit the database transaction, so optionally open a new one here.
|
||||
databaseTransaction(database) {
|
||||
actionOnEnd()
|
||||
}
|
||||
_resultFuture?.setException(t)
|
||||
createTransaction()
|
||||
// TODO Now that we're throwing outside of the suspend the FlowLogic can catch it. We need Quasar to terminate
|
||||
// the fiber when exceptions occur inside a suspend.
|
||||
exceptionDuringSuspend?.let { throw it }
|
||||
logger.trace { "Resumed from $ioRequest" }
|
||||
}
|
||||
|
||||
internal fun resume(scheduler: FiberScheduler) {
|
||||
@ -286,5 +287,4 @@ class FlowStateMachineImpl<R>(override val id: StateMachineRunId,
|
||||
logger.error("Error during resume", t)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,43 @@
|
||||
package net.corda.node.services.statemachine
|
||||
|
||||
import net.corda.core.abbreviate
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
|
||||
interface SessionMessage
|
||||
|
||||
interface ExistingSessionMessage : SessionMessage {
|
||||
val recipientSessionId: Long
|
||||
}
|
||||
|
||||
data class SessionInit(val initiatorSessionId: Long, val flowName: String, val firstPayload: Any?) : SessionMessage
|
||||
|
||||
interface SessionInitResponse : ExistingSessionMessage
|
||||
|
||||
data class SessionConfirm(val initiatorSessionId: Long, val initiatedSessionId: Long) : SessionInitResponse {
|
||||
override val recipientSessionId: Long get() = initiatorSessionId
|
||||
}
|
||||
|
||||
data class SessionReject(val initiatorSessionId: Long, val errorMessage: String) : SessionInitResponse {
|
||||
override val recipientSessionId: Long get() = initiatorSessionId
|
||||
}
|
||||
|
||||
data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage {
|
||||
override fun toString(): String {
|
||||
return "${javaClass.simpleName}(recipientSessionId=$recipientSessionId, payload=${payload.toString().abbreviate(100)})"
|
||||
}
|
||||
}
|
||||
|
||||
data class SessionEnd(override val recipientSessionId: Long) : ExistingSessionMessage
|
||||
|
||||
data class ReceivedSessionMessage<out M : ExistingSessionMessage>(val sender: Party, val message: M)
|
||||
|
||||
fun <T> ReceivedSessionMessage<SessionData>.checkPayloadIs(type: Class<T>): UntrustworthyData<T> {
|
||||
if (type.isInstance(message.payload)) {
|
||||
return UntrustworthyData(type.cast(message.payload))
|
||||
} else {
|
||||
throw FlowException("We were expecting a ${type.name} from $sender but we instead got a " +
|
||||
"${message.payload.javaClass.name} (${message.payload})")
|
||||
}
|
||||
}
|
@ -6,16 +6,16 @@ import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
||||
import co.paralleluniverse.strands.Strand
|
||||
import com.codahale.metrics.Gauge
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import kotlinx.support.jdk8.collections.removeIf
|
||||
import net.corda.core.ThreadBox
|
||||
import net.corda.core.abbreviate
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.commonName
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowStateMachine
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.messaging.ReceivedMessage
|
||||
import net.corda.core.messaging.TopicSession
|
||||
import net.corda.core.messaging.send
|
||||
import net.corda.core.random63BitValue
|
||||
@ -28,15 +28,13 @@ import net.corda.core.utilities.trace
|
||||
import net.corda.node.services.api.Checkpoint
|
||||
import net.corda.node.services.api.CheckpointStorage
|
||||
import net.corda.node.services.api.ServiceHubInternal
|
||||
import net.corda.node.utilities.AddOrRemove
|
||||
import net.corda.node.utilities.AffinityExecutor
|
||||
import net.corda.node.utilities.bufferUntilDatabaseCommit
|
||||
import net.corda.node.utilities.isolatedTransaction
|
||||
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState.Initiated
|
||||
import net.corda.node.services.statemachine.StateMachineManager.FlowSessionState.Initiating
|
||||
import net.corda.node.utilities.*
|
||||
import org.apache.activemq.artemis.utils.ReusableLatch
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
@ -70,7 +68,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
tokenizableServices: List<Any>,
|
||||
val checkpointStorage: CheckpointStorage,
|
||||
val executor: AffinityExecutor,
|
||||
val database: Database) {
|
||||
val database: Database,
|
||||
private val unfinishedFibers: ReusableLatch = ReusableLatch()) {
|
||||
|
||||
inner class FiberScheduler : FiberExecutorScheduler("Same thread scheduler", executor)
|
||||
|
||||
@ -94,8 +93,8 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
val stateMachines = LinkedHashMap<FlowStateMachineImpl<*>, Checkpoint>()
|
||||
val changesPublisher = PublishSubject.create<Change>()
|
||||
|
||||
fun notifyChangeObservers(psm: FlowStateMachineImpl<*>, addOrRemove: AddOrRemove) {
|
||||
changesPublisher.bufferUntilDatabaseCommit().onNext(Change(psm.logic, addOrRemove, psm.id))
|
||||
fun notifyChangeObservers(fiber: FlowStateMachineImpl<*>, addOrRemove: AddOrRemove) {
|
||||
changesPublisher.bufferUntilDatabaseCommit().onNext(Change(fiber.logic, addOrRemove, fiber.id))
|
||||
}
|
||||
})
|
||||
|
||||
@ -103,8 +102,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
@Volatile private var stopping = false
|
||||
// How many Fibers are running and not suspended. If zero and stopping is true, then we are halted.
|
||||
private val liveFibers = ReusableLatch()
|
||||
@VisibleForTesting
|
||||
val unfinishedFibers = ReusableLatch()
|
||||
|
||||
|
||||
// Monitoring support.
|
||||
private val metrics = serviceHub.monitoringService.metrics
|
||||
@ -130,7 +128,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
stateMachines.keys
|
||||
.map { it.logic }
|
||||
.filterIsInstance(flowClass)
|
||||
.map { it to (it.fsm as FlowStateMachineImpl<T>).resultFuture }
|
||||
.map { it to (it.stateMachine as FlowStateMachineImpl<T>).resultFuture }
|
||||
}
|
||||
}
|
||||
|
||||
@ -140,9 +138,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
/**
|
||||
* An observable that emits triples of the changing flow, the type of change, and a process-specific ID number
|
||||
* which may change across restarts.
|
||||
*
|
||||
* We use assignment here so that multiple subscribers share the same wrapped Observable.
|
||||
*/
|
||||
val changes: Observable<Change>
|
||||
get() = mutex.content.changesPublisher
|
||||
val changes: Observable<Change> = mutex.content.changesPublisher.wrapWithDatabaseTransaction()
|
||||
|
||||
init {
|
||||
Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable ->
|
||||
@ -186,9 +185,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
*/
|
||||
fun track(): Pair<List<FlowStateMachineImpl<*>>, Observable<Change>> {
|
||||
return mutex.locked {
|
||||
val bufferedChanges = UnicastSubject.create<Change>()
|
||||
changesPublisher.subscribe(bufferedChanges)
|
||||
Pair(stateMachines.keys.toList(), bufferedChanges)
|
||||
Pair(stateMachines.keys.toList(), changesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,22 +210,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
}
|
||||
serviceHub.networkService.addMessageHandler(sessionTopic) { message, reg ->
|
||||
executor.checkOnThread()
|
||||
val sessionMessage = message.data.deserialize<SessionMessage>()
|
||||
when (sessionMessage) {
|
||||
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage)
|
||||
is SessionInit -> {
|
||||
// TODO SECURITY Look up the party with the full X.500 name instead of just the legal name which
|
||||
// isn't required to be unique
|
||||
// TODO For now have the doorman block signups with identical names, and names with characters that
|
||||
// are used in X.500 name textual serialisation
|
||||
val otherParty = serviceHub.networkMapCache.getNodeByLegalName(message.peer.commonName)?.legalIdentity
|
||||
if (otherParty != null) {
|
||||
onSessionInit(sessionMessage, otherParty)
|
||||
} else {
|
||||
logger.error("Unknown peer ${message.peer} in $sessionMessage")
|
||||
}
|
||||
}
|
||||
}
|
||||
onSessionMessage(message)
|
||||
}
|
||||
}
|
||||
|
||||
@ -241,26 +223,40 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExistingSessionMessage(message: ExistingSessionMessage) {
|
||||
private fun onSessionMessage(message: ReceivedMessage) {
|
||||
val sessionMessage = message.data.deserialize<SessionMessage>()
|
||||
// TODO Look up the party with the full X.500 name instead of just the legal name
|
||||
val sender = serviceHub.networkMapCache.getNodeByLegalName(message.peer.commonName)?.legalIdentity
|
||||
if (sender != null) {
|
||||
when (sessionMessage) {
|
||||
is ExistingSessionMessage -> onExistingSessionMessage(sessionMessage, sender)
|
||||
is SessionInit -> onSessionInit(sessionMessage, sender)
|
||||
}
|
||||
} else {
|
||||
logger.error("Unknown peer ${message.peer} in $sessionMessage")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onExistingSessionMessage(message: ExistingSessionMessage, sender: Party) {
|
||||
val session = openSessions[message.recipientSessionId]
|
||||
if (session != null) {
|
||||
session.psm.logger.trace { "Received $message on $session" }
|
||||
session.fiber.logger.trace { "Received $message on $session" }
|
||||
if (message is SessionEnd) {
|
||||
openSessions.remove(message.recipientSessionId)
|
||||
}
|
||||
session.receivedMessages += message
|
||||
session.receivedMessages += ReceivedSessionMessage(sender, message)
|
||||
if (session.waitingForResponse) {
|
||||
// We only want to resume once, so immediately reset the flag.
|
||||
session.waitingForResponse = false
|
||||
updateCheckpoint(session.psm)
|
||||
resumeFiber(session.psm)
|
||||
updateCheckpoint(session.fiber)
|
||||
resumeFiber(session.fiber)
|
||||
}
|
||||
} else {
|
||||
val otherParty = recentlyClosedSessions.remove(message.recipientSessionId)
|
||||
if (otherParty != null) {
|
||||
val peerParty = recentlyClosedSessions.remove(message.recipientSessionId)
|
||||
if (peerParty != null) {
|
||||
if (message is SessionConfirm) {
|
||||
logger.debug { "Received session confirmation but associated fiber has already terminated, so sending session end" }
|
||||
sendSessionMessage(otherParty, SessionEnd(message.initiatedSessionId), null)
|
||||
sendSessionMessage(peerParty, SessionEnd(message.initiatedSessionId))
|
||||
} else {
|
||||
logger.trace { "Ignoring session end message for already closed session: $message" }
|
||||
}
|
||||
@ -270,32 +266,32 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSessionInit(sessionInit: SessionInit, otherParty: Party) {
|
||||
logger.trace { "Received $sessionInit $otherParty" }
|
||||
private fun onSessionInit(sessionInit: SessionInit, sender: Party) {
|
||||
logger.trace { "Received $sessionInit $sender" }
|
||||
val otherPartySessionId = sessionInit.initiatorSessionId
|
||||
try {
|
||||
val markerClass = Class.forName(sessionInit.flowName)
|
||||
val flowFactory = serviceHub.getFlowFactory(markerClass)
|
||||
if (flowFactory != null) {
|
||||
val flow = flowFactory(otherParty)
|
||||
val psm = createFiber(flow)
|
||||
val session = FlowSession(flow, otherParty, random63BitValue(), otherPartySessionId)
|
||||
val flow = flowFactory(sender)
|
||||
val fiber = createFiber(flow)
|
||||
val session = FlowSession(flow, random63BitValue(), FlowSessionState.Initiated(sender, otherPartySessionId))
|
||||
if (sessionInit.firstPayload != null) {
|
||||
session.receivedMessages += SessionData(session.ourSessionId, sessionInit.firstPayload)
|
||||
session.receivedMessages += ReceivedSessionMessage(sender, SessionData(session.ourSessionId, sessionInit.firstPayload))
|
||||
}
|
||||
openSessions[session.ourSessionId] = session
|
||||
psm.openSessions[Pair(flow, otherParty)] = session
|
||||
updateCheckpoint(psm)
|
||||
sendSessionMessage(otherParty, SessionConfirm(otherPartySessionId, session.ourSessionId), psm)
|
||||
psm.logger.debug { "Initiated from $sessionInit on $session" }
|
||||
startFiber(psm)
|
||||
fiber.openSessions[Pair(flow, sender)] = session
|
||||
updateCheckpoint(fiber)
|
||||
sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), fiber)
|
||||
fiber.logger.debug { "Initiated from $sessionInit on $session" }
|
||||
startFiber(fiber)
|
||||
} else {
|
||||
logger.warn("Unknown flow marker class in $sessionInit")
|
||||
sendSessionMessage(otherParty, SessionReject(otherPartySessionId, "Don't know ${markerClass.name}"), null)
|
||||
sendSessionMessage(sender, SessionReject(otherPartySessionId, "Don't know ${markerClass.name}"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.warn("Received invalid $sessionInit", e)
|
||||
sendSessionMessage(otherParty, SessionReject(otherPartySessionId, "Unable to establish session"), null)
|
||||
sendSessionMessage(sender, SessionReject(otherPartySessionId, "Unable to establish session"))
|
||||
}
|
||||
}
|
||||
|
||||
@ -323,46 +319,47 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
return FlowStateMachineImpl(id, logic, scheduler).apply { initFiber(this) }
|
||||
}
|
||||
|
||||
private fun initFiber(psm: FlowStateMachineImpl<*>) {
|
||||
psm.database = database
|
||||
psm.serviceHub = serviceHub
|
||||
psm.actionOnSuspend = { ioRequest ->
|
||||
updateCheckpoint(psm)
|
||||
private fun initFiber(fiber: FlowStateMachineImpl<*>) {
|
||||
fiber.database = database
|
||||
fiber.serviceHub = serviceHub
|
||||
fiber.actionOnSuspend = { ioRequest ->
|
||||
updateCheckpoint(fiber)
|
||||
// We commit on the fibers transaction that was copied across ThreadLocals during suspend
|
||||
// This will free up the ThreadLocal so on return the caller can carry on with other transactions
|
||||
psm.commitTransaction()
|
||||
fiber.commitTransaction()
|
||||
processIORequest(ioRequest)
|
||||
decrementLiveFibers()
|
||||
}
|
||||
psm.actionOnEnd = {
|
||||
fiber.actionOnEnd = {
|
||||
try {
|
||||
psm.logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
fiber.logic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
mutex.locked {
|
||||
stateMachines.remove(psm)?.let { checkpointStorage.removeCheckpoint(it) }
|
||||
totalFinishedFlows.inc()
|
||||
unfinishedFibers.countDown()
|
||||
notifyChangeObservers(psm, AddOrRemove.REMOVE)
|
||||
stateMachines.remove(fiber)?.let { checkpointStorage.removeCheckpoint(it) }
|
||||
notifyChangeObservers(fiber, AddOrRemove.REMOVE)
|
||||
}
|
||||
endAllFiberSessions(psm)
|
||||
endAllFiberSessions(fiber)
|
||||
} finally {
|
||||
fiber.commitTransaction()
|
||||
decrementLiveFibers()
|
||||
totalFinishedFlows.inc()
|
||||
unfinishedFibers.countDown()
|
||||
}
|
||||
}
|
||||
mutex.locked {
|
||||
totalStartedFlows.inc()
|
||||
unfinishedFibers.countUp()
|
||||
notifyChangeObservers(psm, AddOrRemove.ADD)
|
||||
notifyChangeObservers(fiber, AddOrRemove.ADD)
|
||||
}
|
||||
}
|
||||
|
||||
private fun endAllFiberSessions(psm: FlowStateMachineImpl<*>) {
|
||||
private fun endAllFiberSessions(fiber: FlowStateMachineImpl<*>) {
|
||||
openSessions.values.removeIf { session ->
|
||||
if (session.psm == psm) {
|
||||
val otherPartySessionId = session.otherPartySessionId
|
||||
if (otherPartySessionId != null) {
|
||||
sendSessionMessage(session.otherParty, SessionEnd(otherPartySessionId), psm)
|
||||
if (session.fiber == fiber) {
|
||||
val initiatedState = session.state as? FlowSessionState.Initiated
|
||||
if (initiatedState != null) {
|
||||
sendSessionMessage(initiatedState.peerParty, SessionEnd(initiatedState.peerSessionId), fiber)
|
||||
recentlyClosedSessions[session.ourSessionId] = initiatedState.peerParty
|
||||
}
|
||||
recentlyClosedSessions[session.ourSessionId] = session.otherParty
|
||||
true
|
||||
} else {
|
||||
false
|
||||
@ -393,8 +390,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
* Kicks off a brand new state machine of the given class.
|
||||
* The state machine will be persisted when it suspends, with automated restart if the StateMachineManager is
|
||||
* restarted with checkpointed state machines in the storage service.
|
||||
*
|
||||
* Note that you must be on the [executor] thread.
|
||||
*/
|
||||
fun <T> add(logic: FlowLogic<T>): FlowStateMachine<T> {
|
||||
executor.checkOnThread()
|
||||
// We swap out the parent transaction context as using this frequently leads to a deadlock as we wait
|
||||
// on the flow completion future inside that context. The problem is that any progress checkpoints are
|
||||
// unable to acquire the table lock and move forward till the calling transaction finishes.
|
||||
@ -413,10 +413,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
return fiber
|
||||
}
|
||||
|
||||
private fun updateCheckpoint(psm: FlowStateMachineImpl<*>) {
|
||||
check(psm.state != Strand.State.RUNNING) { "Fiber cannot be running when checkpointing" }
|
||||
val newCheckpoint = Checkpoint(serializeFiber(psm))
|
||||
val previousCheckpoint = mutex.locked { stateMachines.put(psm, newCheckpoint) }
|
||||
private fun updateCheckpoint(fiber: FlowStateMachineImpl<*>) {
|
||||
check(fiber.state != Strand.State.RUNNING) { "Fiber cannot be running when checkpointing" }
|
||||
val newCheckpoint = Checkpoint(serializeFiber(fiber))
|
||||
val previousCheckpoint = mutex.locked { stateMachines.put(fiber, newCheckpoint) }
|
||||
if (previousCheckpoint != null) {
|
||||
checkpointStorage.removeCheckpoint(previousCheckpoint)
|
||||
}
|
||||
@ -424,13 +424,13 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
checkpointingMeter.mark()
|
||||
}
|
||||
|
||||
private fun resumeFiber(psm: FlowStateMachineImpl<*>) {
|
||||
private fun resumeFiber(fiber: FlowStateMachineImpl<*>) {
|
||||
// Avoid race condition when setting stopping to true and then checking liveFibers
|
||||
incrementLiveFibers()
|
||||
if (!stopping) executor.executeASAP {
|
||||
psm.resume(scheduler)
|
||||
fiber.resume(scheduler)
|
||||
} else {
|
||||
psm.logger.debug("Not resuming as SMM is stopping.")
|
||||
fiber.logger.debug("Not resuming as SMM is stopping.")
|
||||
decrementLiveFibers()
|
||||
}
|
||||
}
|
||||
@ -440,59 +440,53 @@ class StateMachineManager(val serviceHub: ServiceHubInternal,
|
||||
if (ioRequest.message is SessionInit) {
|
||||
openSessions[ioRequest.session.ourSessionId] = ioRequest.session
|
||||
}
|
||||
sendSessionMessage(ioRequest.session.otherParty, ioRequest.message, ioRequest.session.psm)
|
||||
sendSessionMessage(ioRequest.session.state.sendToParty, ioRequest.message, ioRequest.session.fiber)
|
||||
if (ioRequest !is ReceiveRequest<*>) {
|
||||
// We sent a message, but don't expect a response, so re-enter the continuation to let it keep going.
|
||||
resumeFiber(ioRequest.session.psm)
|
||||
resumeFiber(ioRequest.session.fiber)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun sendSessionMessage(party: Party, message: SessionMessage, psm: FlowStateMachineImpl<*>?) {
|
||||
val node = serviceHub.networkMapCache.getNodeByCompositeKey(party.owningKey)
|
||||
private fun sendSessionMessage(party: Party, message: SessionMessage, fiber: FlowStateMachineImpl<*>? = null) {
|
||||
val partyInfo = serviceHub.networkMapCache.getPartyInfo(party)
|
||||
?: throw IllegalArgumentException("Don't know about party $party")
|
||||
val logger = psm?.logger ?: logger
|
||||
logger.trace { "Sending $message to party $party" }
|
||||
serviceHub.networkService.send(sessionTopic, message, node.address)
|
||||
val address = serviceHub.networkService.getAddressOfParty(partyInfo)
|
||||
val logger = fiber?.logger ?: logger
|
||||
logger.debug { "Sending $message to party $party, address: $address" }
|
||||
serviceHub.networkService.send(sessionTopic, message, address)
|
||||
}
|
||||
|
||||
|
||||
interface SessionMessage
|
||||
|
||||
interface ExistingSessionMessage : SessionMessage {
|
||||
val recipientSessionId: Long
|
||||
}
|
||||
|
||||
data class SessionInit(val initiatorSessionId: Long, val flowName: String, val firstPayload: Any?) : SessionMessage
|
||||
|
||||
interface SessionInitResponse : ExistingSessionMessage
|
||||
|
||||
data class SessionConfirm(val initiatorSessionId: Long, val initiatedSessionId: Long) : SessionInitResponse {
|
||||
override val recipientSessionId: Long get() = initiatorSessionId
|
||||
}
|
||||
|
||||
data class SessionReject(val initiatorSessionId: Long, val errorMessage: String) : SessionInitResponse {
|
||||
override val recipientSessionId: Long get() = initiatorSessionId
|
||||
}
|
||||
|
||||
data class SessionData(override val recipientSessionId: Long, val payload: Any) : ExistingSessionMessage {
|
||||
override fun toString(): String {
|
||||
return "${javaClass.simpleName}(recipientSessionId=$recipientSessionId, payload=${payload.toString().abbreviate(100)})"
|
||||
/**
|
||||
* [FlowSessionState] describes the session's state.
|
||||
*
|
||||
* [Initiating] is pre-handshake. [Initiating.otherParty] at this point holds a [Party] corresponding to either a
|
||||
* specific peer or a service.
|
||||
* [Initiated] is post-handshake. At this point [Initiating.otherParty] will have been resolved to a specific peer
|
||||
* [Initiated.peerParty], and the peer's sessionId has been initialised.
|
||||
*/
|
||||
sealed class FlowSessionState {
|
||||
abstract val sendToParty: Party
|
||||
class Initiating(
|
||||
val otherParty: Party /** This may be a specific peer or a service party */
|
||||
) : FlowSessionState() {
|
||||
override val sendToParty: Party get() = otherParty
|
||||
}
|
||||
class Initiated(
|
||||
val peerParty: Party, /** This must be a peer party */
|
||||
val peerSessionId: Long
|
||||
) : FlowSessionState() {
|
||||
override val sendToParty: Party get() = peerParty
|
||||
}
|
||||
}
|
||||
|
||||
data class SessionEnd(override val recipientSessionId: Long) : ExistingSessionMessage
|
||||
|
||||
|
||||
data class FlowSession(val flow: FlowLogic<*>,
|
||||
val otherParty: Party,
|
||||
val ourSessionId: Long,
|
||||
var otherPartySessionId: Long?,
|
||||
@Volatile var waitingForResponse: Boolean = false) {
|
||||
|
||||
val receivedMessages = ConcurrentLinkedQueue<ExistingSessionMessage>()
|
||||
val psm: FlowStateMachineImpl<*> get() = flow.fsm as FlowStateMachineImpl<*>
|
||||
|
||||
data class FlowSession(
|
||||
val flow: FlowLogic<*>,
|
||||
val ourSessionId: Long,
|
||||
var state: FlowSessionState,
|
||||
@Volatile var waitingForResponse: Boolean = false
|
||||
) {
|
||||
val receivedMessages = ConcurrentLinkedQueue<ReceivedSessionMessage<ExistingSessionMessage>>()
|
||||
val fiber: FlowStateMachineImpl<*> get() = flow.stateMachine as FlowStateMachineImpl<*>
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.node.services.transactions
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import io.atomix.catalyst.serializer.Serializer
|
||||
import io.atomix.catalyst.transport.Address
|
||||
import io.atomix.catalyst.transport.Transport
|
||||
import io.atomix.catalyst.transport.netty.NettyTransport
|
||||
@ -19,7 +20,7 @@ import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.NodeSSLConfiguration
|
||||
import net.corda.node.services.config.SSLConfiguration
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.CompletableFuture
|
||||
@ -42,7 +43,7 @@ import javax.annotation.concurrent.ThreadSafe
|
||||
*/
|
||||
@ThreadSafe
|
||||
class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterAddresses: List<HostAndPort>,
|
||||
db: Database, config: NodeSSLConfiguration) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
db: Database, config: SSLConfiguration) : UniquenessProvider, SingletonSerializeAsToken() {
|
||||
companion object {
|
||||
private val log = loggerFor<RaftUniquenessProvider>()
|
||||
private val DB_TABLE_NAME = "notary_committed_states"
|
||||
@ -62,11 +63,13 @@ class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterA
|
||||
val address = Address(myAddress.hostText, myAddress.port)
|
||||
val storage = buildStorage(storagePath)
|
||||
val transport = buildTransport(config)
|
||||
val serializer = Serializer()
|
||||
|
||||
val server = CopycatServer.builder(address)
|
||||
.withStateMachine(stateMachineFactory)
|
||||
.withStorage(storage)
|
||||
.withServerTransport(transport)
|
||||
.withSerializer(serializer)
|
||||
.build()
|
||||
|
||||
val serverFuture = if (clusterAddresses.isNotEmpty()) {
|
||||
@ -81,6 +84,7 @@ class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterA
|
||||
val client = CopycatClient.builder(address)
|
||||
.withTransport(transport) // TODO: use local transport for client-server communications
|
||||
.withConnectionStrategy(ConnectionStrategies.EXPONENTIAL_BACKOFF)
|
||||
.withSerializer(serializer)
|
||||
.build()
|
||||
_clientFuture = serverFuture.thenCompose { client.connect(address) }
|
||||
}
|
||||
@ -92,13 +96,13 @@ class RaftUniquenessProvider(storagePath: Path, myAddress: HostAndPort, clusterA
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildTransport(config: NodeSSLConfiguration): Transport? {
|
||||
private fun buildTransport(config: SSLConfiguration): Transport? {
|
||||
return NettyTransport.builder()
|
||||
.withSsl()
|
||||
.withSslProtocol(SslProtocol.TLSv1_2)
|
||||
.withKeyStorePath(config.keyStorePath.toString())
|
||||
.withKeyStorePath(config.keyStoreFile.toString())
|
||||
.withKeyStorePassword(config.keyStorePassword)
|
||||
.withTrustStorePath(config.trustStorePath.toString())
|
||||
.withTrustStorePath(config.trustStoreFile.toString())
|
||||
.withTrustStorePassword(config.trustStorePassword)
|
||||
.build()
|
||||
}
|
||||
|
@ -51,6 +51,11 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
override fun toString() = "$txnId: $note"
|
||||
}
|
||||
|
||||
private object CashBalanceTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_cash_balances") {
|
||||
val currency = varchar("currency", 3)
|
||||
val amount = long("amount")
|
||||
}
|
||||
|
||||
private object TransactionNotesTable : JDBCHashedTable("${NODE_DATABASE_PREFIX}vault_txn_notes") {
|
||||
val txnId = secureHash("txnId").index()
|
||||
val note = text("note")
|
||||
@ -80,43 +85,81 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
}
|
||||
}
|
||||
|
||||
val cashBalances = object : AbstractJDBCHashMap<Currency, Amount<Currency>, CashBalanceTable>(CashBalanceTable) {
|
||||
override fun keyFromRow(row: ResultRow): Currency = Currency.getInstance(row[table.currency])
|
||||
override fun valueFromRow(row: ResultRow): Amount<Currency> = Amount(row[table.amount], keyFromRow(row))
|
||||
|
||||
override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry<Currency, Amount<Currency>>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.currency] = entry.key.currencyCode
|
||||
}
|
||||
|
||||
override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry<Currency, Amount<Currency>>, finalizables: MutableList<() -> Unit>) {
|
||||
insert[table.amount] = entry.value.quantity
|
||||
}
|
||||
}
|
||||
|
||||
val _updatesPublisher = PublishSubject.create<Vault.Update>()
|
||||
val _rawUpdatesPublisher = PublishSubject.create<Vault.Update>()
|
||||
val _updatesInDbTx = _updatesPublisher.wrapWithDatabaseTransaction().asObservable()
|
||||
|
||||
// For use during publishing only.
|
||||
val updatesPublisher: rx.Observer<Vault.Update> get() = _updatesPublisher.bufferUntilDatabaseCommit().tee(_rawUpdatesPublisher)
|
||||
|
||||
fun allUnconsumedStates(): Iterable<StateAndRef<ContractState>> {
|
||||
// Order by txhash for if and when transaction storage has some caching.
|
||||
// Map to StateRef and then to StateAndRef. Use Sequence to avoid conversion to ArrayList that Iterable.map() performs.
|
||||
return unconsumedStates.asSequence().map {
|
||||
fun allUnconsumedStates(): List<StateAndRef<ContractState>> {
|
||||
// Ideally we'd map this transform onto a sequence, but we can't have a lazy list here, since accessing it
|
||||
// from a flow might end up trying to serialize the captured context - vault internal state or db context.
|
||||
return unconsumedStates.map {
|
||||
val storedTx = services.storageService.validatedTransactions.getTransaction(it.txhash) ?: throw Error("Found transaction hash ${it.txhash} in unconsumed contract states that is not in transaction storage.")
|
||||
StateAndRef(storedTx.tx.outputs[it.index], it)
|
||||
}.asIterable()
|
||||
}
|
||||
}
|
||||
|
||||
fun recordUpdate(update: Vault.Update): Vault.Update {
|
||||
if (update != Vault.NoUpdate) {
|
||||
val producedStateRefs = update.produced.map { it.ref }
|
||||
val consumedStateRefs = update.consumed
|
||||
val consumedStateRefs = update.consumed.map { it.ref }
|
||||
log.trace { "Removing $consumedStateRefs consumed contract states and adding $producedStateRefs produced contract states to the database." }
|
||||
unconsumedStates.removeAll(consumedStateRefs)
|
||||
unconsumedStates.addAll(producedStateRefs)
|
||||
}
|
||||
return update
|
||||
}
|
||||
|
||||
// TODO: consider moving this logic outside the vault
|
||||
fun maybeUpdateCashBalances(update: Vault.Update) {
|
||||
if (update.containsType<Cash.State>()) {
|
||||
val consumed = sumCashStates(update.consumed)
|
||||
val produced = sumCashStates(update.produced)
|
||||
(produced.keys + consumed.keys).map { currency ->
|
||||
val producedAmount = produced[currency] ?: Amount(0, currency)
|
||||
val consumedAmount = consumed[currency] ?: Amount(0, currency)
|
||||
val currentBalance = cashBalances[currency] ?: Amount(0, currency)
|
||||
cashBalances[currency] = currentBalance + producedAmount - consumedAmount
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun sumCashStates(states: Iterable<StateAndRef<ContractState>>): Map<Currency, Amount<Currency>> {
|
||||
return states.mapNotNull { (it.state.data as? FungibleAsset<Currency>)?.amount }
|
||||
.groupBy { it.token.product }
|
||||
.mapValues { it.value.map { Amount(it.quantity, it.token.product) }.sumOrThrow() }
|
||||
}
|
||||
})
|
||||
|
||||
override val cashBalances: Map<Currency, Amount<Currency>> get() = mutex.locked { HashMap(cashBalances) }
|
||||
|
||||
override val currentVault: Vault get() = mutex.locked { Vault(allUnconsumedStates()) }
|
||||
|
||||
override val rawUpdates: Observable<Vault.Update>
|
||||
get() = mutex.locked { _rawUpdatesPublisher }
|
||||
|
||||
override val updates: Observable<Vault.Update>
|
||||
get() = mutex.locked { _updatesPublisher }
|
||||
get() = mutex.locked { _updatesInDbTx }
|
||||
|
||||
override fun track(): Pair<Vault, Observable<Vault.Update>> {
|
||||
return mutex.locked {
|
||||
Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed())
|
||||
Pair(Vault(allUnconsumedStates()), _updatesPublisher.bufferUntilSubscribed().wrapWithDatabaseTransaction())
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,16 +171,16 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
override val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
||||
get() = currentVault.states.filterStatesOfType<LinearState>().associateBy { it.state.data.linearId }.mapValues { it.value }
|
||||
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>): Vault {
|
||||
override fun notifyAll(txns: Iterable<WireTransaction>) {
|
||||
val ourKeys = services.keyManagementService.keys.keys
|
||||
val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, netDelta, ourKeys) }
|
||||
if (netDelta != Vault.NoUpdate) {
|
||||
mutex.locked {
|
||||
recordUpdate(netDelta)
|
||||
maybeUpdateCashBalances(netDelta)
|
||||
updatesPublisher.onNext(netDelta)
|
||||
}
|
||||
}
|
||||
return currentVault
|
||||
}
|
||||
|
||||
override fun addNoteToTransaction(txnId: SecureHash, noteText: String) {
|
||||
@ -278,22 +321,27 @@ class NodeVaultService(private val services: ServiceHub) : SingletonSerializeAsT
|
||||
map { tx.outRef<ContractState>(it.data) }
|
||||
|
||||
// Now calculate the states that are being spent by this transaction.
|
||||
val consumed = tx.inputs.toHashSet()
|
||||
val consumedRefs = tx.inputs.toHashSet()
|
||||
// We use Guava union here as it's lazy for contains() which is how retainAll() is implemented.
|
||||
// i.e. retainAll() iterates over consumed, checking contains() on the parameter. Sets.union() does not physically create
|
||||
// a new collection and instead contains() just checks the contains() of both parameters, and so we don't end up
|
||||
// iterating over all (a potentially very large) unconsumedStates at any point.
|
||||
mutex.locked {
|
||||
consumed.retainAll(Sets.union(netDelta.produced, unconsumedStates))
|
||||
consumedRefs.retainAll(Sets.union(netDelta.produced, unconsumedStates))
|
||||
}
|
||||
|
||||
// Is transaction irrelevant?
|
||||
if (consumed.isEmpty() && ourNewStates.isEmpty()) {
|
||||
if (consumedRefs.isEmpty() && ourNewStates.isEmpty()) {
|
||||
log.trace { "tx ${tx.id} was irrelevant to this vault, ignoring" }
|
||||
return Vault.NoUpdate
|
||||
}
|
||||
|
||||
return Vault.Update(consumed, ourNewStates.toHashSet())
|
||||
val consumedStates = consumedRefs.map {
|
||||
val state = services.loadState(it)
|
||||
StateAndRef(state, it)
|
||||
}.toSet()
|
||||
|
||||
return Vault.Update(consumedStates, ourNewStates.toHashSet())
|
||||
}
|
||||
|
||||
private fun isRelevant(state: ContractState, ourKeys: Set<PublicKey>): Boolean {
|
||||
|
@ -12,7 +12,9 @@ import org.jetbrains.exposed.sql.*
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionInterface
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import rx.Observable
|
||||
import rx.Subscriber
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.Subject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.io.Closeable
|
||||
import java.security.PublicKey
|
||||
@ -23,6 +25,7 @@ import java.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Table prefix for all tables owned by the node module.
|
||||
@ -119,13 +122,13 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
||||
|
||||
val manager: StrandLocalTransactionManager get() = databaseToInstance[database]!!
|
||||
|
||||
val transactionBoundaries: PublishSubject<Boundary> get() = manager._transactionBoundaries
|
||||
val transactionBoundaries: Subject<Boundary, Boundary> get() = manager._transactionBoundaries
|
||||
}
|
||||
|
||||
|
||||
data class Boundary(val txId: UUID)
|
||||
|
||||
private val _transactionBoundaries = PublishSubject.create<Boundary>()
|
||||
private val _transactionBoundaries = PublishSubject.create<Boundary>().toSerialized()
|
||||
|
||||
init {
|
||||
// Found a unit test that was forgetting to close the database transactions. When you close() on the top level
|
||||
@ -150,7 +153,7 @@ class StrandLocalTransactionManager(initWithDatabase: Database) : TransactionMan
|
||||
override fun currentOrNull(): Transaction? = threadLocalTx.get()
|
||||
|
||||
// Direct copy of [ThreadLocalTransaction].
|
||||
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>, val transactionBoundaries: PublishSubject<Boundary>) : TransactionInterface {
|
||||
private class StrandLocalTransaction(override val db: Database, isolation: Int, val threadLocal: ThreadLocal<Transaction>, val transactionBoundaries: Subject<Boundary, Boundary>) : TransactionInterface {
|
||||
val id = UUID.randomUUID()
|
||||
|
||||
override val connection: Connection by lazy(LazyThreadSafetyMode.NONE) {
|
||||
@ -200,6 +203,74 @@ fun <T : Any> rx.Observer<T>.bufferUntilDatabaseCommit(): rx.Observer<T> {
|
||||
return subject
|
||||
}
|
||||
|
||||
// A subscriber that delegates to multiple others, wrapping a database transaction around the combination.
|
||||
private class DatabaseTransactionWrappingSubscriber<U>(val db: Database?) : Subscriber<U>() {
|
||||
// Some unsubscribes happen inside onNext() so need something that supports concurrent modification.
|
||||
val delegates = CopyOnWriteArrayList<Subscriber<in U>>()
|
||||
|
||||
fun forEachSubscriberWithDbTx(block: Subscriber<in U>.() -> Unit) {
|
||||
databaseTransaction(db ?: StrandLocalTransactionManager.database) {
|
||||
delegates.filter { !it.isUnsubscribed }.forEach {
|
||||
it.block()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCompleted() {
|
||||
forEachSubscriberWithDbTx { onCompleted() }
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable?) {
|
||||
forEachSubscriberWithDbTx { onError(e) }
|
||||
}
|
||||
|
||||
override fun onNext(s: U) {
|
||||
forEachSubscriberWithDbTx { onNext(s) }
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
forEachSubscriberWithDbTx { onStart() }
|
||||
}
|
||||
|
||||
fun cleanUp() {
|
||||
if (delegates.removeIf { it.isUnsubscribed }) {
|
||||
if (delegates.isEmpty()) {
|
||||
unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A subscriber that wraps another but does not pass on observations to it.
|
||||
private class NoOpSubscriber<U>(t: Subscriber<in U>) : Subscriber<U>(t) {
|
||||
override fun onCompleted() {
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable?) {
|
||||
}
|
||||
|
||||
override fun onNext(s: U) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap delivery of observations in a database transaction. Multiple subscribers will receive the observations inside
|
||||
* the same database transaction. This also lazily subscribes to the source [rx.Observable] to preserve any buffering
|
||||
* that might be in place.
|
||||
*/
|
||||
fun <T : Any> rx.Observable<T>.wrapWithDatabaseTransaction(db: Database? = null): rx.Observable<T> {
|
||||
val wrappingSubscriber = DatabaseTransactionWrappingSubscriber<T>(db)
|
||||
// Use lift to add subscribers to a special subscriber that wraps a database transaction around observations.
|
||||
// Each subscriber will be passed to this lambda when they subscribe, at which point we add them to wrapping subscriber.
|
||||
return this.lift { toBeWrappedInDbTx: Subscriber<in T> ->
|
||||
// Add the subscriber to the wrapping subscriber, which will invoke the original subscribers together inside a database transaction.
|
||||
wrappingSubscriber.delegates.add(toBeWrappedInDbTx)
|
||||
// If we are the first subscriber, return the shared subscriber, otherwise return a subscriber that does nothing.
|
||||
if (wrappingSubscriber.delegates.size == 1) wrappingSubscriber else NoOpSubscriber<T>(toBeWrappedInDbTx)
|
||||
// Clean up the shared list of subscribers when they unsubscribe.
|
||||
}.doOnUnsubscribe { wrappingSubscriber.cleanUp() }
|
||||
}
|
||||
|
||||
// Composite columns for use with below Exposed helpers.
|
||||
data class PartyColumns(val name: Column<String>, val owningKey: Column<CompositeKey>)
|
||||
data class StateRefColumns(val txId: Column<SecureHash>, val index: Column<Int>)
|
||||
|
@ -18,6 +18,14 @@ import kotlin.system.measureTimeMillis
|
||||
* access patterns and performance requirements.
|
||||
*/
|
||||
|
||||
/**
|
||||
* The default maximum size of the LRU cache.
|
||||
*
|
||||
* TODO: make this value configurable
|
||||
* TODO: tune this value, as it's currently mostly a guess
|
||||
*/
|
||||
val DEFAULT_MAX_BUCKETS = 4096
|
||||
|
||||
/**
|
||||
* A convenient JDBC table backed hash map with iteration order based on insertion order.
|
||||
* See [AbstractJDBCHashMap] for further implementation details.
|
||||
@ -28,7 +36,7 @@ import kotlin.system.measureTimeMillis
|
||||
*/
|
||||
class JDBCHashMap<K : Any, V : Any>(tableName: String,
|
||||
loadOnInit: Boolean = false,
|
||||
maxBuckets: Int = 256)
|
||||
maxBuckets: Int = DEFAULT_MAX_BUCKETS)
|
||||
: AbstractJDBCHashMap<K, V, JDBCHashMap.BlobMapTable>(BlobMapTable(tableName), loadOnInit, maxBuckets) {
|
||||
|
||||
class BlobMapTable(tableName: String) : JDBCHashedTable(tableName) {
|
||||
@ -78,7 +86,7 @@ fun <T : Any> deserializeFromBlob(blob: Blob): T = bytesFromBlob<T>(blob).deseri
|
||||
*/
|
||||
class JDBCHashSet<K : Any>(tableName: String,
|
||||
loadOnInit: Boolean = false,
|
||||
maxBuckets: Int = 256)
|
||||
maxBuckets: Int = DEFAULT_MAX_BUCKETS)
|
||||
: AbstractJDBCHashSet<K, JDBCHashSet.BlobSetTable>(BlobSetTable(tableName), loadOnInit, maxBuckets) {
|
||||
|
||||
class BlobSetTable(tableName: String) : JDBCHashedTable(tableName) {
|
||||
@ -100,7 +108,7 @@ class JDBCHashSet<K : Any>(tableName: String,
|
||||
*/
|
||||
abstract class AbstractJDBCHashSet<K : Any, out T : JDBCHashedTable>(protected val table: T,
|
||||
loadOnInit: Boolean = false,
|
||||
maxBuckets: Int = 256) : MutableSet<K>, AbstractSet<K>() {
|
||||
maxBuckets: Int = DEFAULT_MAX_BUCKETS) : MutableSet<K>, AbstractSet<K>() {
|
||||
protected val innerMap = object : AbstractJDBCHashMap<K, Unit, T>(table, loadOnInit, maxBuckets) {
|
||||
override fun keyFromRow(row: ResultRow): K = this@AbstractJDBCHashSet.elementFromRow(row)
|
||||
|
||||
@ -194,7 +202,7 @@ abstract class AbstractJDBCHashSet<K : Any, out T : JDBCHashedTable>(protected v
|
||||
*/
|
||||
abstract class AbstractJDBCHashMap<K : Any, V : Any, out T : JDBCHashedTable>(val table: T,
|
||||
val loadOnInit: Boolean = false,
|
||||
val maxBuckets: Int = 256) : MutableMap<K, V>, AbstractMap<K, V>() {
|
||||
val maxBuckets: Int = DEFAULT_MAX_BUCKETS) : MutableMap<K, V>, AbstractMap<K, V>() {
|
||||
|
||||
companion object {
|
||||
protected val log = loggerFor<AbstractJDBCHashMap<*, *, *>>()
|
||||
|
@ -25,49 +25,53 @@ import java.time.LocalDateTime
|
||||
* the java.time API, some core types, and Kotlin data classes.
|
||||
*/
|
||||
object JsonSupport {
|
||||
|
||||
fun createDefaultMapper(identities: IdentityService): ObjectMapper {
|
||||
val mapper = ServiceHubObjectMapper(identities)
|
||||
mapper.enable(SerializationFeature.INDENT_OUTPUT)
|
||||
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
mapper.enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||
|
||||
val timeModule = SimpleModule("java.time")
|
||||
timeModule.addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
timeModule.addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||
timeModule.addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||
timeModule.addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||
|
||||
val cordaModule = SimpleModule("core")
|
||||
cordaModule.addSerializer(Party::class.java, PartySerializer)
|
||||
cordaModule.addDeserializer(Party::class.java, PartyDeserializer)
|
||||
cordaModule.addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
cordaModule.addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
cordaModule.addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||
cordaModule.addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
cordaModule.addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
// For ed25519 pubkeys
|
||||
cordaModule.addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
|
||||
cordaModule.addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
|
||||
|
||||
// For composite keys
|
||||
cordaModule.addSerializer(CompositeKey::class.java, CompositeKeySerializer)
|
||||
cordaModule.addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
|
||||
|
||||
// For NodeInfo
|
||||
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
||||
cordaModule.addSerializer(NodeInfo::class.java, NodeInfoSerializer)
|
||||
cordaModule.addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
|
||||
|
||||
mapper.registerModule(timeModule)
|
||||
mapper.registerModule(cordaModule)
|
||||
mapper.registerModule(KotlinModule())
|
||||
return mapper
|
||||
val javaTimeModule : Module by lazy {
|
||||
SimpleModule("java.time").apply {
|
||||
addSerializer(LocalDate::class.java, ToStringSerializer)
|
||||
addDeserializer(LocalDate::class.java, LocalDateDeserializer)
|
||||
addKeyDeserializer(LocalDate::class.java, LocalDateKeyDeserializer)
|
||||
addSerializer(LocalDateTime::class.java, ToStringSerializer)
|
||||
}
|
||||
}
|
||||
|
||||
val cordaModule : Module by lazy {
|
||||
SimpleModule("core").apply {
|
||||
addSerializer(Party::class.java, PartySerializer)
|
||||
addDeserializer(Party::class.java, PartyDeserializer)
|
||||
addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
// It's slightly remarkable, but apparently Jackson works out that this is the only possibility
|
||||
// for a SecureHash at the moment and tries to use SHA256 directly even though we only give it SecureHash
|
||||
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
|
||||
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
|
||||
|
||||
// For ed25519 pubkeys
|
||||
addSerializer(EdDSAPublicKey::class.java, PublicKeySerializer)
|
||||
addDeserializer(EdDSAPublicKey::class.java, PublicKeyDeserializer)
|
||||
|
||||
// For composite keys
|
||||
addSerializer(CompositeKey::class.java, CompositeKeySerializer)
|
||||
addDeserializer(CompositeKey::class.java, CompositeKeyDeserializer)
|
||||
|
||||
// For NodeInfo
|
||||
// TODO this tunnels the Kryo representation as a Base58 encoded string. Replace when RPC supports this.
|
||||
addSerializer(NodeInfo::class.java, NodeInfoSerializer)
|
||||
addDeserializer(NodeInfo::class.java, NodeInfoDeserializer)
|
||||
}
|
||||
}
|
||||
|
||||
fun createDefaultMapper(identities: IdentityService): ObjectMapper =
|
||||
ServiceHubObjectMapper(identities).apply {
|
||||
enable(SerializationFeature.INDENT_OUTPUT)
|
||||
enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY)
|
||||
enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
|
||||
|
||||
registerModule(javaTimeModule)
|
||||
registerModule(cordaModule)
|
||||
registerModule(KotlinModule())
|
||||
}
|
||||
|
||||
class ServiceHubObjectMapper(val identities: IdentityService) : ObjectMapper()
|
||||
|
||||
object ToStringSerializer : JsonSerializer<Any>() {
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.composite
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
@ -24,7 +25,7 @@ object ServiceIdentityGenerator {
|
||||
* @param threshold The threshold for the generated group [CompositeKey.Node].
|
||||
*/
|
||||
fun generateToDisk(dirs: List<Path>, serviceId: String, serviceName: String, threshold: Int = 1) {
|
||||
log.trace("Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}")
|
||||
log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" }
|
||||
|
||||
val keyPairs = (1..dirs.size).map { generateKeyPair() }
|
||||
val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public.composite }).build(threshold)
|
||||
@ -32,8 +33,8 @@ object ServiceIdentityGenerator {
|
||||
|
||||
keyPairs.zip(dirs) { keyPair, dir ->
|
||||
Files.createDirectories(dir)
|
||||
val privateKeyFile = serviceId + "-private-key"
|
||||
val publicKeyFile = serviceId + "-public"
|
||||
val privateKeyFile = "$serviceId-private-key"
|
||||
val publicKeyFile = "$serviceId-public"
|
||||
notaryParty.writeToFile(dir.resolve(publicKeyFile))
|
||||
keyPair.serialize().writeToFile(dir.resolve(privateKeyFile))
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
package net.corda.node.utilities.certsigning
|
||||
|
||||
import joptsimple.OptionParser
|
||||
import net.corda.core.*
|
||||
import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA
|
||||
@ -9,12 +8,12 @@ import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA
|
||||
import net.corda.core.crypto.X509Utilities.addOrReplaceCertificate
|
||||
import net.corda.core.crypto.X509Utilities.addOrReplaceKey
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.node.services.config.ConfigHelper
|
||||
import net.corda.node.ArgsParser
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.getValue
|
||||
import net.corda.node.utilities.certsigning.CertificateSigner.Companion.log
|
||||
import java.net.URL
|
||||
import java.nio.file.Paths
|
||||
import java.security.KeyPair
|
||||
import java.security.cert.Certificate
|
||||
import kotlin.system.exitProcess
|
||||
@ -32,16 +31,16 @@ class CertificateSigner(val config: NodeConfiguration, val certService: Certific
|
||||
}
|
||||
|
||||
fun buildKeyStore() {
|
||||
config.certificatesPath.createDirectories()
|
||||
config.certificatesDirectory.createDirectories()
|
||||
|
||||
val caKeyStore = X509Utilities.loadOrCreateKeyStore(config.keyStorePath, config.keyStorePassword)
|
||||
val caKeyStore = X509Utilities.loadOrCreateKeyStore(config.keyStoreFile, config.keyStorePassword)
|
||||
|
||||
if (!caKeyStore.containsAlias(CORDA_CLIENT_CA)) {
|
||||
// No certificate found in key store, create certificate signing request and post request to signing server.
|
||||
log.info("No certificate found in key store, creating certificate signing request...")
|
||||
|
||||
// Create or load key pair from the key store.
|
||||
val keyPair = X509Utilities.loadOrCreateKeyPairFromKeyStore(config.keyStorePath, config.keyStorePassword,
|
||||
val keyPair = X509Utilities.loadOrCreateKeyPairFromKeyStore(config.keyStoreFile, config.keyStorePassword,
|
||||
config.keyStorePassword, CORDA_CLIENT_CA_PRIVATE_KEY) {
|
||||
X509Utilities.createSelfSignedCACert(config.myLegalName)
|
||||
}
|
||||
@ -59,15 +58,15 @@ class CertificateSigner(val config: NodeConfiguration, val certService: Certific
|
||||
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||
caKeyStore.addOrReplaceCertificate(CORDA_CLIENT_CA, certificates.first())
|
||||
|
||||
X509Utilities.saveKeyStore(caKeyStore, config.keyStorePath, config.keyStorePassword)
|
||||
X509Utilities.saveKeyStore(caKeyStore, config.keyStoreFile, config.keyStorePassword)
|
||||
|
||||
// Save certificates to trust store.
|
||||
val trustStore = X509Utilities.loadOrCreateKeyStore(config.trustStorePath, config.trustStorePassword)
|
||||
val trustStore = X509Utilities.loadOrCreateKeyStore(config.trustStoreFile, config.trustStorePassword)
|
||||
|
||||
// Assumes certificate chain always starts with client certificate and end with root certificate.
|
||||
trustStore.addOrReplaceCertificate(CORDA_ROOT_CA, certificates.last())
|
||||
|
||||
X509Utilities.saveKeyStore(trustStore, config.trustStorePath, config.trustStorePassword)
|
||||
X509Utilities.saveKeyStore(trustStore, config.trustStoreFile, config.trustStorePassword)
|
||||
} else {
|
||||
log.trace("Certificate already exists, exiting certificate signer...")
|
||||
}
|
||||
@ -97,7 +96,7 @@ class CertificateSigner(val config: NodeConfiguration, val certService: Certific
|
||||
* @return Request ID return from the server.
|
||||
*/
|
||||
private fun submitCertificateSigningRequest(keyPair: KeyPair): String {
|
||||
val requestIdStore = config.certificatesPath / "certificate-request-id.txt"
|
||||
val requestIdStore = config.certificatesDirectory / "certificate-request-id.txt"
|
||||
// Retrieve request id from file if exists, else post a request to server.
|
||||
return if (!requestIdStore.exists()) {
|
||||
val request = X509Utilities.createCertificateSigningRequest(config.myLegalName, config.nearestCity, config.emailAddress, keyPair)
|
||||
@ -112,30 +111,21 @@ class CertificateSigner(val config: NodeConfiguration, val certService: Certific
|
||||
}
|
||||
}
|
||||
|
||||
object ParamsSpec {
|
||||
val parser = OptionParser()
|
||||
val baseDirectoryArg = parser.accepts("base-dir", "Working directory of Corda Node.").withRequiredArg().defaultsTo(".")
|
||||
val configFileArg = parser.accepts("config-file", "The path to the config file.").withRequiredArg()
|
||||
}
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
val argsParser = ArgsParser()
|
||||
val cmdlineOptions = try {
|
||||
ParamsSpec.parser.parse(*args)
|
||||
argsParser.parse(*args)
|
||||
} catch (ex: Exception) {
|
||||
CertificateSigner.log.error("Unable to parse args", ex)
|
||||
ParamsSpec.parser.printHelpOn(System.out)
|
||||
log.error("Unable to parse args", ex)
|
||||
argsParser.printHelp(System.out)
|
||||
exitProcess(1)
|
||||
}
|
||||
val baseDirectoryPath = Paths.get(cmdlineOptions.valueOf(ParamsSpec.baseDirectoryArg))
|
||||
val configFile = if (cmdlineOptions.has(ParamsSpec.configFileArg)) Paths.get(cmdlineOptions.valueOf(ParamsSpec.configFileArg)) else null
|
||||
|
||||
val config = ConfigHelper.loadConfig(baseDirectoryPath, configFile, allowMissingConfig = true).let { config ->
|
||||
object : NodeConfiguration by FullNodeConfiguration(config) {
|
||||
val certificateSigningService: URL by config
|
||||
}
|
||||
val config = cmdlineOptions.loadConfig(allowMissingConfig = true)
|
||||
val configuration = object : NodeConfiguration by FullNodeConfiguration(cmdlineOptions.baseDirectory, config) {
|
||||
val certificateSigningService: URL by config
|
||||
}
|
||||
|
||||
// TODO: Use HTTPS instead
|
||||
CertificateSigner(config, HTTPCertificateSigningService(config.certificateSigningService)).buildKeyStore()
|
||||
CertificateSigner(configuration, HTTPCertificateSigningService(configuration.certificateSigningService)).buildKeyStore()
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,7 @@ keyStorePassword = "cordacadevpass"
|
||||
trustStorePassword = "trustpass"
|
||||
dataSourceProperties = {
|
||||
dataSourceClassName = org.h2.jdbcx.JdbcDataSource
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=0;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.url" = "jdbc:h2:file:"${basedir}"/persistence;DB_CLOSE_ON_EXIT=FALSE;LOCK_TIMEOUT=10000;WRITE_DELAY=100;AUTO_SERVER_PORT="${h2port}
|
||||
"dataSource.user" = sa
|
||||
"dataSource.password" = ""
|
||||
}
|
||||
|
86
node/src/test/kotlin/net/corda/node/ArgsParserTest.kt
Normal file
86
node/src/test/kotlin/net/corda/node/ArgsParserTest.kt
Normal file
@ -0,0 +1,86 @@
|
||||
package net.corda.node
|
||||
|
||||
import joptsimple.OptionException
|
||||
import net.corda.core.div
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatExceptionOfType
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
|
||||
class ArgsParserTest {
|
||||
private val parser = ArgsParser()
|
||||
private val workingDirectory = Paths.get(".").normalize().toAbsolutePath()
|
||||
|
||||
@Test
|
||||
fun `no command line arguments`() {
|
||||
assertThat(parser.parse()).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = workingDirectory,
|
||||
configFile = workingDirectory / "node.conf",
|
||||
help = false,
|
||||
logToConsole = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `just base-directory with relative path`() {
|
||||
val expectedBaseDir = Paths.get("tmp").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--base-directory", "tmp")
|
||||
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = expectedBaseDir,
|
||||
configFile = expectedBaseDir / "node.conf",
|
||||
help = false,
|
||||
logToConsole = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `just base-directory with absolute path`() {
|
||||
val baseDirectory = Paths.get("tmp").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--base-directory", baseDirectory.toString())
|
||||
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = baseDirectory,
|
||||
configFile = baseDirectory / "node.conf",
|
||||
help = false,
|
||||
logToConsole = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `just config-file with relative path`() {
|
||||
val cmdLineOptions = parser.parse("--config-file", "different.conf")
|
||||
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = workingDirectory,
|
||||
configFile = workingDirectory / "different.conf",
|
||||
help = false,
|
||||
logToConsole = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `just config-file with absolute path`() {
|
||||
val configFile = Paths.get("tmp", "a.conf").normalize().toAbsolutePath()
|
||||
val cmdLineOptions = parser.parse("--config-file", configFile.toString())
|
||||
assertThat(cmdLineOptions).isEqualTo(CmdLineOptions(
|
||||
baseDirectory = workingDirectory,
|
||||
configFile = configFile,
|
||||
help = false,
|
||||
logToConsole = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both base-directory and config-file`() {
|
||||
assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy {
|
||||
parser.parse("--base-directory", "base", "--config-file", "conf")
|
||||
}.withMessageContaining("base-directory").withMessageContaining("config-file")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `base-directory without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--base-directory")
|
||||
}.withMessageContaining("base-directory")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `config-file without argument`() {
|
||||
assertThatExceptionOfType(OptionException::class.java).isThrownBy {
|
||||
parser.parse("--config-file")
|
||||
}.withMessageContaining("config-file")
|
||||
}
|
||||
}
|
@ -50,9 +50,11 @@ class CordaRPCOpsImplTest {
|
||||
rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database)
|
||||
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(startFlowPermission<CashFlow>())))
|
||||
|
||||
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
||||
transactions = rpc.verifiedTransactions().second
|
||||
vaultUpdates = rpc.vaultAndUpdates().second
|
||||
databaseTransaction(aliceNode.database) {
|
||||
stateMachineUpdates = rpc.stateMachinesAndUpdates().second
|
||||
transactions = rpc.verifiedTransactions().second
|
||||
vaultUpdates = rpc.vaultAndUpdates().second
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -59,7 +59,6 @@ import kotlin.test.assertTrue
|
||||
* We assume that Alice and Bob already found each other via some market, and have agreed the details already.
|
||||
*/
|
||||
class TwoPartyTradeFlowTests {
|
||||
|
||||
lateinit var net: MockNetwork
|
||||
lateinit var notaryNode: MockNetwork.MockNode
|
||||
lateinit var aliceNode: MockNetwork.MockNode
|
||||
@ -85,7 +84,7 @@ class TwoPartyTradeFlowTests {
|
||||
net = MockNetwork(false, true)
|
||||
|
||||
ledger {
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name, ALICE_KEY)
|
||||
bobNode = net.createPartyNode(notaryNode.info.address, BOB.name, BOB_KEY)
|
||||
val aliceKey = aliceNode.services.legalIdentityKey
|
||||
@ -125,7 +124,7 @@ class TwoPartyTradeFlowTests {
|
||||
@Test
|
||||
fun `shutdown and restore`() {
|
||||
ledger {
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name, ALICE_KEY)
|
||||
bobNode = net.createPartyNode(notaryNode.info.address, BOB.name, BOB_KEY)
|
||||
aliceNode.disableDBCloseOnStop()
|
||||
@ -133,7 +132,7 @@ class TwoPartyTradeFlowTests {
|
||||
val aliceKey = aliceNode.services.legalIdentityKey
|
||||
val notaryKey = notaryNode.services.notaryIdentityKey
|
||||
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.Handle
|
||||
val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.PeerHandle
|
||||
val networkMapAddr = notaryNode.info.address
|
||||
|
||||
net.runNetwork() // Clear network map registration messages
|
||||
@ -235,7 +234,7 @@ class TwoPartyTradeFlowTests {
|
||||
|
||||
@Test
|
||||
fun `check dependencies of sale asset are resolved`() {
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name, ALICE_KEY)
|
||||
bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name, BOB_KEY)
|
||||
val aliceKey = aliceNode.services.legalIdentityKey
|
||||
@ -327,7 +326,7 @@ class TwoPartyTradeFlowTests {
|
||||
@Test
|
||||
fun `track() works`() {
|
||||
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name, ALICE_KEY)
|
||||
bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name, BOB_KEY)
|
||||
val aliceKey = aliceNode.services.legalIdentityKey
|
||||
@ -416,10 +415,10 @@ class TwoPartyTradeFlowTests {
|
||||
private fun runBuyerAndSeller(assetToSell: StateAndRef<OwnableState>): RunResult {
|
||||
val buyerFuture = bobNode.initiateSingleShotFlow(Seller::class) { otherParty ->
|
||||
Buyer(otherParty, notaryNode.info.notaryIdentity, 1000.DOLLARS, CommercialPaper.State::class.java)
|
||||
}.map { it.fsm }
|
||||
}.map { it.stateMachine }
|
||||
val seller = Seller(bobNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS, ALICE_KEY)
|
||||
val sellerResultFuture = aliceNode.smm.add(seller).resultFuture
|
||||
return RunResult(buyerFuture, sellerResultFuture, seller.fsm.id)
|
||||
val sellerResultFuture = aliceNode.services.startFlow(seller).resultFuture
|
||||
return RunResult(buyerFuture, sellerResultFuture, seller.stateMachine.id)
|
||||
}
|
||||
|
||||
private fun LedgerDSL<TestTransactionDSLInterpreter, TestLedgerDSLInterpreter>.runWithError(
|
||||
@ -427,7 +426,7 @@ class TwoPartyTradeFlowTests {
|
||||
aliceError: Boolean,
|
||||
expectedMessageSubstring: String
|
||||
) {
|
||||
notaryNode = net.createNotaryNode(DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name, DUMMY_NOTARY_KEY)
|
||||
aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name, ALICE_KEY)
|
||||
bobNode = net.createPartyNode(notaryNode.info.address, BOB.name, BOB_KEY)
|
||||
val aliceKey = aliceNode.services.legalIdentityKey
|
@ -29,14 +29,14 @@ class InMemoryNetworkMapCacheTest {
|
||||
val nodeB = network.createNode(null, -1, MockNetwork.DefaultFactory, true, "Node B", keyPair, ServiceInfo(NetworkMapService.type))
|
||||
|
||||
// Node A currently knows only about itself, so this returns node A
|
||||
assertEquals(nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite), nodeA.info)
|
||||
assertEquals(nodeA.netMapCache.getNodeByLegalIdentityKey(keyPair.public.composite), nodeA.info)
|
||||
|
||||
databaseTransaction(nodeA.database) {
|
||||
nodeA.netMapCache.addNode(nodeB.info)
|
||||
}
|
||||
// Now both nodes match, so it throws an error
|
||||
expect<IllegalStateException> {
|
||||
nodeA.netMapCache.getNodeByCompositeKey(keyPair.public.composite)
|
||||
nodeA.netMapCache.getNodeByLegalIdentityKey(keyPair.public.composite)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,9 @@ open class MockServiceHubInternal(
|
||||
|
||||
override fun recordTransactions(txs: Iterable<SignedTransaction>) = recordTransactionsInternal(txStorageService, txs)
|
||||
|
||||
override fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> = smm.add(logic)
|
||||
override fun <T> startFlow(logic: FlowLogic<T>): FlowStateMachine<T> {
|
||||
return smm.executor.fetchFrom { smm.add(logic) }
|
||||
}
|
||||
|
||||
override fun registerFlowInitiator(markerClass: KClass<*>, flowFactory: (Party) -> FlowLogic<*>) {
|
||||
flowFactories[markerClass.java] = flowFactory
|
||||
|
@ -38,7 +38,6 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
|
||||
val realClock: Clock = Clock.systemUTC()
|
||||
val stoppedClock = Clock.fixed(realClock.instant(), realClock.zone)
|
||||
val testClock = TestClock(stoppedClock)
|
||||
@ -81,7 +80,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() {
|
||||
|
||||
databaseTransaction(database) {
|
||||
val kms = MockKeyManagementService(ALICE_KEY)
|
||||
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.Handle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database)
|
||||
val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.PeerHandle(0, "None"), AffinityExecutor.ServiceAffinityExecutor("test", 1), database)
|
||||
services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference {
|
||||
override val testReference = this@NodeSchedulerServiceTest
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.node.services.ServiceInfo
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.flows.NotaryChangeFlow.Instigator
|
||||
@ -22,6 +23,7 @@ import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class NotaryChangeTests {
|
||||
lateinit var net: MockNetwork
|
||||
@ -86,6 +88,60 @@ class NotaryChangeTests {
|
||||
assertThat(ex.error).isInstanceOf(StateReplacementRefused::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should not break encumbrance links`() {
|
||||
val issueTx = issueEncumberedState(clientNodeA, oldNotaryNode)
|
||||
|
||||
val state = StateAndRef(issueTx.outputs.first(), StateRef(issueTx.id, 0))
|
||||
val newNotary = newNotaryNode.info.notaryIdentity
|
||||
val flow = Instigator(state, newNotary)
|
||||
val future = clientNodeA.services.startFlow(flow)
|
||||
net.runNetwork()
|
||||
val newState = future.resultFuture.getOrThrow()
|
||||
assertEquals(newState.state.notary, newNotary)
|
||||
|
||||
val notaryChangeTx = clientNodeA.services.storageService.validatedTransactions.getTransaction(newState.ref.txhash)!!.tx
|
||||
|
||||
// Check that all encumbrances have been propagated to the outputs
|
||||
val originalOutputs = issueTx.outputs.map { it.data }
|
||||
val newOutputs = notaryChangeTx.outputs.map { it.data }
|
||||
assertTrue(originalOutputs.minus(newOutputs).isEmpty())
|
||||
|
||||
// Check that encumbrance links aren't broken after notary change
|
||||
val encumbranceLink = HashMap<ContractState, ContractState?>()
|
||||
issueTx.outputs.forEach {
|
||||
val currentState = it.data
|
||||
val encumbranceState = it.encumbrance?.let { issueTx.outputs[it].data }
|
||||
encumbranceLink[currentState] = encumbranceState
|
||||
}
|
||||
notaryChangeTx.outputs.forEach {
|
||||
val currentState = it.data
|
||||
val encumbranceState = it.encumbrance?.let { notaryChangeTx.outputs[it].data }
|
||||
assertEquals(encumbranceLink[currentState], encumbranceState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun issueEncumberedState(node: AbstractNode, notaryNode: AbstractNode): WireTransaction {
|
||||
val owner = node.info.legalIdentity.ref(0)
|
||||
val notary = notaryNode.info.notaryIdentity
|
||||
|
||||
val stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey)
|
||||
val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey)
|
||||
val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party.owningKey)
|
||||
|
||||
val tx = TransactionType.General.Builder(null).apply {
|
||||
addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey))
|
||||
addOutputState(stateA, notary, encumbrance = 2) // Encumbered by stateB
|
||||
addOutputState(stateC, notary)
|
||||
addOutputState(stateB, notary, encumbrance = 1) // Encumbered by stateC
|
||||
}
|
||||
val nodeKey = node.services.legalIdentityKey
|
||||
tx.signWith(nodeKey)
|
||||
val stx = tx.toSignedTransaction()
|
||||
node.services.recordTransactions(listOf(stx))
|
||||
return tx.toWireTransaction()
|
||||
}
|
||||
|
||||
// TODO: Add more test cases once we have a general flow/service exception handling mechanism:
|
||||
// - A participant is offline/can't be found on the network
|
||||
// - The requesting party is not a participant
|
||||
|
@ -85,7 +85,7 @@ class NotaryServiceTests {
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.TimestampInvalid::class.java)
|
||||
}
|
||||
|
||||
@Test fun `should report conflict for a duplicate transaction`() {
|
||||
@Test fun `should sign identical transaction multiple times (signing is idempotent)`() {
|
||||
val stx = run {
|
||||
val inputState = issueState(clientNode)
|
||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||
@ -93,8 +93,32 @@ class NotaryServiceTests {
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
val firstAttempt = NotaryFlow.Client(stx)
|
||||
val secondAttempt = NotaryFlow.Client(stx)
|
||||
val f1 = clientNode.services.startFlow(firstAttempt)
|
||||
val f2 = clientNode.services.startFlow(secondAttempt)
|
||||
|
||||
net.runNetwork()
|
||||
|
||||
assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow())
|
||||
}
|
||||
|
||||
@Test fun `should report conflict when inputs are reused across transactions`() {
|
||||
val inputState = issueState(clientNode)
|
||||
val stx = run {
|
||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||
tx.signWith(clientNode.keyPair!!)
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
val stx2 = run {
|
||||
val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState)
|
||||
tx.addInputState(issueState(clientNode))
|
||||
tx.signWith(clientNode.keyPair!!)
|
||||
tx.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
val firstSpend = NotaryFlow.Client(stx)
|
||||
val secondSpend = NotaryFlow.Client(stx)
|
||||
val secondSpend = NotaryFlow.Client(stx2) // Double spend the inputState in a second transaction.
|
||||
clientNode.services.startFlow(firstSpend)
|
||||
val future = clientNode.services.startFlow(secondSpend)
|
||||
|
||||
@ -102,11 +126,10 @@ class NotaryServiceTests {
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() }
|
||||
val notaryError = ex.error as NotaryError.Conflict
|
||||
assertEquals(notaryError.tx, stx.tx)
|
||||
assertEquals(notaryError.tx, stx2.tx)
|
||||
notaryError.conflict.verified()
|
||||
}
|
||||
|
||||
|
||||
private fun runNotaryClient(stx: SignedTransaction): ListenableFuture<DigitalSignature.WithKey> {
|
||||
val flow = NotaryFlow.Client(stx)
|
||||
val future = clientNode.services.startFlow(flow).resultFuture
|
||||
|
@ -5,6 +5,7 @@ import net.corda.node.services.config.FullNodeConfiguration
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Test
|
||||
import java.nio.file.Paths
|
||||
|
||||
class RPCUserServiceImplTest {
|
||||
|
||||
@ -69,6 +70,6 @@ class RPCUserServiceImplTest {
|
||||
}
|
||||
|
||||
private fun loadWithContents(configString: String): RPCUserServiceImpl {
|
||||
return RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.parseString(configString)))
|
||||
return RPCUserServiceImpl(FullNodeConfiguration(Paths.get("."), ConfigFactory.parseString(configString)))
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.node.CordaPluginRegistry
|
||||
@ -20,7 +19,6 @@ import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Ignore
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
@ -128,10 +126,7 @@ class ScheduledFlowTests {
|
||||
assertTrue("Must be processed", stateFromB.state.data.processed)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@Test
|
||||
// TODO I need to investigate why we get very very occasional SessionInit failures
|
||||
// during notarisation.
|
||||
fun `Run a whole batch of scheduled flows`() {
|
||||
val N = 100
|
||||
for (i in 0..N - 1) {
|
||||
|
@ -52,7 +52,7 @@ class ValidatingNotaryServiceTests {
|
||||
val future = runClient(stx)
|
||||
|
||||
val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() }
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.TransactionInvalid::class.java)
|
||||
assertThat(ex.error).isInstanceOf(NotaryError.SignaturesInvalid::class.java)
|
||||
}
|
||||
|
||||
@Test fun `should report error for missing signatures`() {
|
||||
@ -73,7 +73,7 @@ class ValidatingNotaryServiceTests {
|
||||
val notaryError = ex.error
|
||||
assertThat(notaryError).isInstanceOf(NotaryError.SignaturesMissing::class.java)
|
||||
|
||||
val missingKeys = (notaryError as NotaryError.SignaturesMissing).missingSigners
|
||||
val missingKeys = (notaryError as NotaryError.SignaturesMissing).cause.missing
|
||||
assertEquals(setOf(expectedMissingKey), missingKeys)
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
package net.corda.node.services
|
||||
package net.corda.node.services.messaging
|
||||
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigFactory.empty
|
||||
import net.corda.core.crypto.composite
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.messaging.Message
|
||||
@ -12,17 +12,18 @@ import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.messaging.createMessage
|
||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import net.corda.core.utilities.LogHelper
|
||||
import net.corda.node.services.RPCUserService
|
||||
import net.corda.node.services.RPCUserServiceImpl
|
||||
import net.corda.node.services.config.FullNodeConfiguration
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.node.services.config.configureWithDevSSLCertificate
|
||||
import net.corda.node.services.messaging.ArtemisMessagingServer
|
||||
import net.corda.node.services.messaging.NodeMessagingClient
|
||||
import net.corda.node.services.network.InMemoryNetworkMapCache
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.PersistentUniquenessProvider
|
||||
import net.corda.node.utilities.AffinityExecutor.ServiceAffinityExecutor
|
||||
import net.corda.node.utilities.configureDatabase
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.TestNodeConfiguration
|
||||
import net.corda.testing.freeLocalHostAndPort
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
@ -35,13 +36,13 @@ import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.io.Closeable
|
||||
import java.net.ServerSocket
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.LinkedBlockingQueue
|
||||
import java.util.concurrent.TimeUnit.MILLISECONDS
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
|
||||
//TODO This needs to be merged into P2PMessagingTest as that creates a more realistic environment
|
||||
class ArtemisMessagingTests {
|
||||
@Rule @JvmField val temporaryFolder = TemporaryFolder()
|
||||
|
||||
@ -66,18 +67,12 @@ class ArtemisMessagingTests {
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
userService = RPCUserServiceImpl(FullNodeConfiguration(ConfigFactory.empty()))
|
||||
// TODO: create a base class that provides a default implementation
|
||||
config = object : NodeConfiguration {
|
||||
override val basedir: Path = temporaryFolder.newFolder().toPath()
|
||||
override val myLegalName: String = "me"
|
||||
override val nearestCity: String = "London"
|
||||
override val emailAddress: String = ""
|
||||
override val devMode: Boolean = true
|
||||
override val exportJMXto: String = ""
|
||||
override val keyStorePassword: String = "testpass"
|
||||
override val trustStorePassword: String = "trustpass"
|
||||
}
|
||||
val baseDirectory = temporaryFolder.root.toPath()
|
||||
userService = RPCUserServiceImpl(FullNodeConfiguration(baseDirectory, empty()))
|
||||
config = TestNodeConfiguration(
|
||||
baseDirectory = baseDirectory,
|
||||
myLegalName = "me",
|
||||
networkMapService = null)
|
||||
LogHelper.setLevel(PersistentUniquenessProvider::class)
|
||||
val dataSourceAndDatabase = configureDatabase(makeTestDataSourceProperties())
|
||||
dataSource = dataSourceAndDatabase.first
|
||||
@ -169,8 +164,7 @@ class ArtemisMessagingTests {
|
||||
fun `client should be able to send large numbers of messages to itself before network map is available and survive restart, then receive messages`() {
|
||||
// Crank the iteration up as high as you want... just takes longer to run.
|
||||
val iterations = 100
|
||||
val settableFuture: SettableFuture<Unit> = SettableFuture.create()
|
||||
networkMapRegistrationFuture = settableFuture
|
||||
networkMapRegistrationFuture = SettableFuture.create()
|
||||
|
||||
val receivedMessages = LinkedBlockingQueue<Message>()
|
||||
|
@ -1,11 +1,11 @@
|
||||
package net.corda.node.services.persistence
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.NullPublicKey
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
@ -109,8 +109,7 @@ class DBTransactionStorageTests {
|
||||
|
||||
@Test
|
||||
fun `updates are fired`() {
|
||||
val future = SettableFuture.create<SignedTransaction>()
|
||||
transactionStorage.updates.subscribe { tx -> future.set(tx) }
|
||||
val future = transactionStorage.updates.toFuture()
|
||||
val expected = newTransaction()
|
||||
databaseTransaction(database) {
|
||||
transactionStorage.addTransaction(expected)
|
||||
|
@ -2,23 +2,36 @@ package net.corda.node.services.statemachine
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import co.paralleluniverse.strands.Strand.UncaughtExceptionHandler
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.DOLLARS
|
||||
import net.corda.core.contracts.issuedBy
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.generateKeyPair
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.FlowSessionException
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.map
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.core.rootCause
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.flows.CashCommand
|
||||
import net.corda.flows.CashFlow
|
||||
import net.corda.flows.NotaryFlow
|
||||
import net.corda.node.services.persistence.checkpoints
|
||||
import net.corda.node.services.statemachine.StateMachineManager.*
|
||||
import net.corda.node.utilities.databaseTransaction
|
||||
import net.corda.testing.expect
|
||||
import net.corda.testing.expectEvents
|
||||
import net.corda.testing.initiateSingleShotFlow
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork
|
||||
import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import net.corda.testing.node.MockNetwork.MockNode
|
||||
import net.corda.testing.sequence
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -29,17 +42,24 @@ import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class StateMachineManagerTests {
|
||||
|
||||
private val net = MockNetwork()
|
||||
private val net = MockNetwork(servicePeerAllocationStrategy = InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin())
|
||||
private val sessionTransfers = ArrayList<SessionTransfer>()
|
||||
private lateinit var node1: MockNode
|
||||
private lateinit var node2: MockNode
|
||||
private lateinit var notary1: MockNode
|
||||
private lateinit var notary2: MockNode
|
||||
|
||||
@Before
|
||||
fun start() {
|
||||
val nodes = net.createTwoNodes()
|
||||
node1 = nodes.first
|
||||
node2 = nodes.second
|
||||
val notaryKeyPair = generateKeyPair()
|
||||
// Note that these notaries don't operate correctly as they don't share their state. They are only used for testing
|
||||
// service addressing.
|
||||
notary1 = net.createNotaryNode(networkMapAddr = node1.services.myInfo.address, keyPair = notaryKeyPair, serviceName = "notary-service-2000")
|
||||
notary2 = net.createNotaryNode(networkMapAddr = node1.services.myInfo.address, keyPair = notaryKeyPair, serviceName = "notary-service-2000")
|
||||
|
||||
net.messagingNetwork.receivedMessages.toSessionTransfers().forEach { sessionTransfers += it }
|
||||
net.runNetwork()
|
||||
}
|
||||
@ -51,7 +71,7 @@ class StateMachineManagerTests {
|
||||
|
||||
@Test
|
||||
fun `newly added flow is preserved on restart`() {
|
||||
node1.smm.add(NoOpFlow(nonTerminating = true))
|
||||
node1.services.startFlow(NoOpFlow(nonTerminating = true))
|
||||
node1.acceptableLiveFiberCountOnStop = 1
|
||||
val restoredFlow = node1.restartAndGetRestoredFlow<NoOpFlow>()
|
||||
assertThat(restoredFlow.flowStarted).isTrue()
|
||||
@ -64,15 +84,40 @@ class StateMachineManagerTests {
|
||||
@Suspendable
|
||||
override fun call() = Unit
|
||||
}
|
||||
node1.smm.add(flow)
|
||||
node1.services.startFlow(flow)
|
||||
assertThat(flow.lazyTime).isNotNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exception while fiber suspended`() {
|
||||
node2.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(2, it) }
|
||||
val flow = ReceiveFlow(node2.info.legalIdentity)
|
||||
val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl
|
||||
// Before the flow runs change the suspend action to throw an exception
|
||||
val exceptionDuringSuspend = Exception("Thrown during suspend")
|
||||
fiber.actionOnSuspend = {
|
||||
throw exceptionDuringSuspend
|
||||
}
|
||||
var uncaughtException: Throwable? = null
|
||||
fiber.uncaughtExceptionHandler = UncaughtExceptionHandler { f, e ->
|
||||
uncaughtException = e
|
||||
}
|
||||
net.runNetwork()
|
||||
assertThatThrownBy {
|
||||
fiber.resultFuture.getOrThrow()
|
||||
}.isSameAs(exceptionDuringSuspend)
|
||||
assertThat(node1.smm.allStateMachines).isEmpty()
|
||||
// Make sure it doesn't get swallowed up
|
||||
assertThat(uncaughtException?.rootCause).isSameAs(exceptionDuringSuspend)
|
||||
// Make sure the fiber does actually terminate
|
||||
assertThat(fiber.isTerminated).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flow restarted just after receiving payload`() {
|
||||
node2.services.registerFlowInitiator(SendFlow::class) { ReceiveThenSuspendFlow(it) }
|
||||
node2.services.registerFlowInitiator(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
val payload = random63BitValue()
|
||||
node1.smm.add(SendFlow(payload, node2.info.legalIdentity))
|
||||
node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity))
|
||||
|
||||
// We push through just enough messages to get only the payload sent
|
||||
node2.pumpReceive()
|
||||
@ -80,7 +125,7 @@ class StateMachineManagerTests {
|
||||
node2.acceptableLiveFiberCountOnStop = 1
|
||||
node2.stop()
|
||||
net.runNetwork()
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<ReceiveThenSuspendFlow>(node1)
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<ReceiveFlow>(node1)
|
||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo(payload)
|
||||
}
|
||||
|
||||
@ -88,7 +133,7 @@ class StateMachineManagerTests {
|
||||
fun `flow added before network map does run after init`() {
|
||||
val node3 = net.createNode(node1.info.address) //create vanilla node
|
||||
val flow = NoOpFlow()
|
||||
node3.smm.add(flow)
|
||||
node3.services.startFlow(flow)
|
||||
assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet
|
||||
net.runNetwork() // Allow network map messages to flow
|
||||
assertEquals(true, flow.flowStarted) // Now we should have run the flow
|
||||
@ -98,7 +143,7 @@ class StateMachineManagerTests {
|
||||
fun `flow added before network map will be init checkpointed`() {
|
||||
var node3 = net.createNode(node1.info.address) //create vanilla node
|
||||
val flow = NoOpFlow()
|
||||
node3.smm.add(flow)
|
||||
node3.services.startFlow(flow)
|
||||
assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet
|
||||
node3.disableDBCloseOnStop()
|
||||
node3.stop()
|
||||
@ -122,13 +167,13 @@ class StateMachineManagerTests {
|
||||
@Test
|
||||
fun `flow loaded from checkpoint will respond to messages from before start`() {
|
||||
val payload = random63BitValue()
|
||||
node1.services.registerFlowInitiator(ReceiveThenSuspendFlow::class) { SendFlow(payload, it) }
|
||||
node2.smm.add(ReceiveThenSuspendFlow(node1.info.legalIdentity)) // Prepare checkpointed receive flow
|
||||
node1.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(payload, it) }
|
||||
node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow
|
||||
// Make sure the add() has finished initial processing.
|
||||
node2.smm.executor.flush()
|
||||
node2.disableDBCloseOnStop()
|
||||
node2.stop() // kill receiver
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<ReceiveThenSuspendFlow>(node1)
|
||||
val restoredFlow = node2.restartAndGetRestoredFlow<ReceiveFlow>(node1)
|
||||
assertThat(restoredFlow.receivedPayloads[0]).isEqualTo(payload)
|
||||
}
|
||||
|
||||
@ -145,7 +190,7 @@ class StateMachineManagerTests {
|
||||
net.runNetwork()
|
||||
|
||||
// Kick off first send and receive
|
||||
node2.smm.add(PingPongFlow(node3.info.legalIdentity, payload))
|
||||
node2.services.startFlow(PingPongFlow(node3.info.legalIdentity, payload))
|
||||
databaseTransaction(node2.database) {
|
||||
assertEquals(1, node2.checkpointStorage.checkpoints().size)
|
||||
}
|
||||
@ -186,27 +231,27 @@ class StateMachineManagerTests {
|
||||
fun `sending to multiple parties`() {
|
||||
val node3 = net.createNode(node1.info.address)
|
||||
net.runNetwork()
|
||||
node2.services.registerFlowInitiator(SendFlow::class) { ReceiveThenSuspendFlow(it) }
|
||||
node3.services.registerFlowInitiator(SendFlow::class) { ReceiveThenSuspendFlow(it) }
|
||||
node2.services.registerFlowInitiator(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
node3.services.registerFlowInitiator(SendFlow::class) { ReceiveFlow(it).nonTerminating() }
|
||||
val payload = random63BitValue()
|
||||
node1.smm.add(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity))
|
||||
node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity))
|
||||
net.runNetwork()
|
||||
val node2Flow = node2.getSingleFlow<ReceiveThenSuspendFlow>().first
|
||||
val node3Flow = node3.getSingleFlow<ReceiveThenSuspendFlow>().first
|
||||
val node2Flow = node2.getSingleFlow<ReceiveFlow>().first
|
||||
val node3Flow = node3.getSingleFlow<ReceiveFlow>().first
|
||||
assertThat(node2Flow.receivedPayloads[0]).isEqualTo(payload)
|
||||
assertThat(node3Flow.receivedPayloads[0]).isEqualTo(payload)
|
||||
|
||||
assertSessionTransfers(node2,
|
||||
node1 sent sessionInit(SendFlow::class, payload) to node2,
|
||||
node2 sent sessionConfirm() to node1,
|
||||
node1 sent sessionEnd() to node2
|
||||
node2 sent sessionConfirm to node1,
|
||||
node1 sent sessionEnd to node2
|
||||
//There's no session end from the other flows as they're manually suspended
|
||||
)
|
||||
|
||||
assertSessionTransfers(node3,
|
||||
node1 sent sessionInit(SendFlow::class, payload) to node3,
|
||||
node3 sent sessionConfirm() to node1,
|
||||
node1 sent sessionEnd() to node3
|
||||
node3 sent sessionConfirm to node1,
|
||||
node1 sent sessionEnd to node3
|
||||
//There's no session end from the other flows as they're manually suspended
|
||||
)
|
||||
|
||||
@ -220,56 +265,113 @@ class StateMachineManagerTests {
|
||||
net.runNetwork()
|
||||
val node2Payload = random63BitValue()
|
||||
val node3Payload = random63BitValue()
|
||||
node2.services.registerFlowInitiator(ReceiveThenSuspendFlow::class) { SendFlow(node2Payload, it) }
|
||||
node3.services.registerFlowInitiator(ReceiveThenSuspendFlow::class) { SendFlow(node3Payload, it) }
|
||||
val multiReceiveFlow = ReceiveThenSuspendFlow(node2.info.legalIdentity, node3.info.legalIdentity)
|
||||
node1.smm.add(multiReceiveFlow)
|
||||
node2.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(node2Payload, it) }
|
||||
node3.services.registerFlowInitiator(ReceiveFlow::class) { SendFlow(node3Payload, it) }
|
||||
val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating()
|
||||
node1.services.startFlow(multiReceiveFlow)
|
||||
node1.acceptableLiveFiberCountOnStop = 1
|
||||
net.runNetwork()
|
||||
assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(node2Payload)
|
||||
assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload)
|
||||
|
||||
assertSessionTransfers(node2,
|
||||
node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node2,
|
||||
node2 sent sessionConfirm() to node1,
|
||||
node1 sent sessionInit(ReceiveFlow::class) to node2,
|
||||
node2 sent sessionConfirm to node1,
|
||||
node2 sent sessionData(node2Payload) to node1,
|
||||
node2 sent sessionEnd() to node1
|
||||
node2 sent sessionEnd to node1
|
||||
)
|
||||
|
||||
assertSessionTransfers(node3,
|
||||
node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node3,
|
||||
node3 sent sessionConfirm() to node1,
|
||||
node1 sent sessionInit(ReceiveFlow::class) to node3,
|
||||
node3 sent sessionConfirm to node1,
|
||||
node3 sent sessionData(node3Payload) to node1,
|
||||
node3 sent sessionEnd() to node1
|
||||
node3 sent sessionEnd to node1
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both sides do a send as their first IO request`() {
|
||||
node2.services.registerFlowInitiator(PingPongFlow::class) { PingPongFlow(it, 20L) }
|
||||
node1.smm.add(PingPongFlow(node2.info.legalIdentity, 10L))
|
||||
node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L))
|
||||
net.runNetwork()
|
||||
|
||||
assertSessionTransfers(
|
||||
node1 sent sessionInit(PingPongFlow::class, 10L) to node2,
|
||||
node2 sent sessionConfirm() to node1,
|
||||
node2 sent sessionConfirm to node1,
|
||||
node2 sent sessionData(20L) to node1,
|
||||
node1 sent sessionData(11L) to node2,
|
||||
node2 sent sessionData(21L) to node1,
|
||||
node1 sent sessionEnd() to node2
|
||||
node1 sent sessionEnd to node2
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `different notaries are picked when addressing shared notary identity`() {
|
||||
assertEquals(notary1.info.notaryIdentity, notary2.info.notaryIdentity)
|
||||
node1.services.startFlow(CashFlow(CashCommand.IssueCash(
|
||||
DOLLARS(2000),
|
||||
OpaqueBytes.of(0x01),
|
||||
node1.info.legalIdentity,
|
||||
notary1.info.notaryIdentity)))
|
||||
// We pay a couple of times, the notary picking should go round robin
|
||||
for (i in 1 .. 3) {
|
||||
node1.services.startFlow(CashFlow(CashCommand.PayCash(
|
||||
DOLLARS(500).issuedBy(node1.info.legalIdentity.ref(0x01)),
|
||||
node2.info.legalIdentity)))
|
||||
net.runNetwork()
|
||||
}
|
||||
val endpoint = net.messagingNetwork.endpoint(notary1.net.myAddress as InMemoryMessagingNetwork.PeerHandle)!!
|
||||
val notary1Address: MessageRecipients = endpoint.getAddressOfParty(notary1.services.networkMapCache.getPartyInfo(notary1.info.notaryIdentity)!!)
|
||||
assert(notary1Address is InMemoryMessagingNetwork.ServiceHandle)
|
||||
assertEquals(notary1Address, endpoint.getAddressOfParty(notary2.services.networkMapCache.getPartyInfo(notary2.info.notaryIdentity)!!))
|
||||
sessionTransfers.expectEvents(isStrict = false) {
|
||||
sequence(
|
||||
// First Pay
|
||||
expect(match = { it.message is SessionInit && it.message.flowName == NotaryFlow.Client::class.java.name }) {
|
||||
it.message as SessionInit
|
||||
assertEquals(node1.id, it.from)
|
||||
assertEquals(notary1Address, it.to)
|
||||
},
|
||||
expect(match = { it.message is SessionConfirm }) {
|
||||
it.message as SessionConfirm
|
||||
assertEquals(notary1.id, it.from)
|
||||
},
|
||||
// Second pay
|
||||
expect(match = { it.message is SessionInit && it.message.flowName == NotaryFlow.Client::class.java.name }) {
|
||||
it.message as SessionInit
|
||||
assertEquals(node1.id, it.from)
|
||||
assertEquals(notary1Address, it.to)
|
||||
},
|
||||
expect(match = { it.message is SessionConfirm }) {
|
||||
it.message as SessionConfirm
|
||||
assertEquals(notary2.id, it.from)
|
||||
},
|
||||
// Third pay
|
||||
expect(match = { it.message is SessionInit && it.message.flowName == NotaryFlow.Client::class.java.name }) {
|
||||
it.message as SessionInit
|
||||
assertEquals(node1.id, it.from)
|
||||
assertEquals(notary1Address, it.to)
|
||||
},
|
||||
expect(match = { it.message is SessionConfirm }) {
|
||||
it.message as SessionConfirm
|
||||
require(it.from == notary1.id)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `exception thrown on other side`() {
|
||||
node2.services.registerFlowInitiator(ReceiveThenSuspendFlow::class) { ExceptionFlow }
|
||||
val future = node1.smm.add(ReceiveThenSuspendFlow(node2.info.legalIdentity)).resultFuture
|
||||
val erroringFiber = node2.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow }.map { it.stateMachine as FlowStateMachineImpl }
|
||||
val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl
|
||||
net.runNetwork()
|
||||
assertThatThrownBy { future.getOrThrow() }.isInstanceOf(FlowSessionException::class.java)
|
||||
assertThatThrownBy { receivingFiber.resultFuture.getOrThrow() }.isInstanceOf(FlowException::class.java)
|
||||
assertThat(receivingFiber.isTerminated).isTrue()
|
||||
assertThat(erroringFiber.getOrThrow().isTerminated).isTrue()
|
||||
assertSessionTransfers(
|
||||
node1 sent sessionInit(ReceiveThenSuspendFlow::class) to node2,
|
||||
node2 sent sessionConfirm() to node1,
|
||||
node2 sent sessionEnd() to node1
|
||||
node1 sent sessionInit(ReceiveFlow::class) to node2,
|
||||
node2 sent sessionConfirm to node1,
|
||||
node2 sent sessionEnd to node1
|
||||
)
|
||||
}
|
||||
|
||||
@ -290,22 +392,22 @@ class StateMachineManagerTests {
|
||||
|
||||
private fun sessionInit(flowMarker: KClass<*>, payload: Any? = null) = SessionInit(0, flowMarker.java.name, payload)
|
||||
|
||||
private fun sessionConfirm() = SessionConfirm(0, 0)
|
||||
private val sessionConfirm = SessionConfirm(0, 0)
|
||||
|
||||
private fun sessionData(payload: Any) = SessionData(0, payload)
|
||||
|
||||
private fun sessionEnd() = SessionEnd(0)
|
||||
private val sessionEnd = SessionEnd(0)
|
||||
|
||||
private fun assertSessionTransfers(vararg expected: SessionTransfer) {
|
||||
assertThat(sessionTransfers).containsExactly(*expected)
|
||||
}
|
||||
|
||||
private fun assertSessionTransfers(node: MockNode, vararg expected: SessionTransfer) {
|
||||
val actualForNode = sessionTransfers.filter { it.from == node.id || it.to == node.id }
|
||||
val actualForNode = sessionTransfers.filter { it.from == node.id || it.to == node.net.myAddress }
|
||||
assertThat(actualForNode).containsExactly(*expected)
|
||||
}
|
||||
|
||||
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: Int) {
|
||||
private data class SessionTransfer(val from: Int, val message: SessionMessage, val to: MessageRecipients) {
|
||||
val isPayloadTransfer: Boolean get() = message is SessionData || message is SessionInit && message.firstPayload != null
|
||||
override fun toString(): String = "$from sent $message to $to"
|
||||
}
|
||||
@ -314,8 +416,7 @@ class StateMachineManagerTests {
|
||||
return filter { it.message.topicSession == StateMachineManager.sessionTopic }.map {
|
||||
val from = it.sender.id
|
||||
val message = it.message.data.deserialize<SessionMessage>()
|
||||
val to = (it.recipients as InMemoryMessagingNetwork.Handle).id
|
||||
SessionTransfer(from, sanitise(message), to)
|
||||
SessionTransfer(from, sanitise(message), it.recipients)
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,11 +431,10 @@ class StateMachineManagerTests {
|
||||
}
|
||||
|
||||
private infix fun MockNode.sent(message: SessionMessage): Pair<Int, SessionMessage> = Pair(id, message)
|
||||
private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.id)
|
||||
private infix fun Pair<Int, SessionMessage>.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress)
|
||||
|
||||
|
||||
private class NoOpFlow(val nonTerminating: Boolean = false) : FlowLogic<Unit>() {
|
||||
|
||||
@Transient var flowStarted = false
|
||||
|
||||
@Suspendable
|
||||
@ -348,7 +448,6 @@ class StateMachineManagerTests {
|
||||
|
||||
|
||||
private class SendFlow(val payload: Any, vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||
|
||||
init {
|
||||
require(otherParties.isNotEmpty())
|
||||
}
|
||||
@ -358,7 +457,8 @@ class StateMachineManagerTests {
|
||||
}
|
||||
|
||||
|
||||
private class ReceiveThenSuspendFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||
private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic<Unit>() {
|
||||
private var nonTerminating: Boolean = false
|
||||
|
||||
init {
|
||||
require(otherParties.isNotEmpty())
|
||||
@ -369,12 +469,18 @@ class StateMachineManagerTests {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
receivedPayloads = otherParties.map { receive<Any>(it).unwrap { it } }
|
||||
Fiber.park()
|
||||
if (nonTerminating) {
|
||||
Fiber.park()
|
||||
}
|
||||
}
|
||||
|
||||
fun nonTerminating(): ReceiveFlow {
|
||||
nonTerminating = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
private class PingPongFlow(val otherParty: Party, val payload: Long) : FlowLogic<Unit>() {
|
||||
|
||||
@Transient var receivedPayload: Long? = null
|
||||
@Transient var receivedPayload2: Long? = null
|
||||
|
||||
@ -388,5 +494,4 @@ class StateMachineManagerTests {
|
||||
private object ExceptionFlow : FlowLogic<Nothing>() {
|
||||
override fun call(): Nothing = throw Exception()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,24 +1,43 @@
|
||||
package net.corda.node.utilities
|
||||
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.bufferUntilSubscribed
|
||||
import net.corda.core.tee
|
||||
import net.corda.testing.node.makeTestDataSourceProperties
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.sql.transactions.TransactionManager
|
||||
import org.junit.After
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import java.io.Closeable
|
||||
import java.util.*
|
||||
|
||||
class ObservablesTests {
|
||||
|
||||
private fun isInDatabaseTransaction(): Boolean = (TransactionManager.currentOrNull() != null)
|
||||
|
||||
val toBeClosed = mutableListOf<Closeable>()
|
||||
|
||||
fun createDatabase(): Database {
|
||||
val (closeable, database) = configureDatabase(makeTestDataSourceProperties())
|
||||
toBeClosed += closeable
|
||||
return database
|
||||
}
|
||||
|
||||
@After
|
||||
fun after() {
|
||||
toBeClosed.forEach { it.close() }
|
||||
toBeClosed.clear()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bufferUntilDatabaseCommit delays until transaction closed`() {
|
||||
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||
val database = createDatabase()
|
||||
|
||||
val subject = PublishSubject.create<Int>()
|
||||
val observable: Observable<Int> = subject
|
||||
val source = PublishSubject.create<Int>()
|
||||
val observable: Observable<Int> = source
|
||||
|
||||
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
@ -27,10 +46,10 @@ class ObservablesTests {
|
||||
observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) }
|
||||
|
||||
databaseTransaction(database) {
|
||||
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||
val delayedSubject = source.bufferUntilDatabaseCommit()
|
||||
assertThat(source).isNotEqualTo(delayedSubject)
|
||||
delayedSubject.onNext(0)
|
||||
subject.onNext(1)
|
||||
source.onNext(1)
|
||||
assertThat(firstEvent.isDone).isTrue()
|
||||
assertThat(secondEvent.isDone).isFalse()
|
||||
}
|
||||
@ -38,16 +57,14 @@ class ObservablesTests {
|
||||
|
||||
assertThat(firstEvent.get()).isEqualTo(1 to true)
|
||||
assertThat(secondEvent.get()).isEqualTo(0 to false)
|
||||
|
||||
toBeClosed.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bufferUntilDatabaseCommit delays until transaction closed repeatable`() {
|
||||
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||
val database = createDatabase()
|
||||
|
||||
val subject = PublishSubject.create<Int>()
|
||||
val observable: Observable<Int> = subject
|
||||
val source = PublishSubject.create<Int>()
|
||||
val observable: Observable<Int> = source
|
||||
|
||||
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
val secondEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
@ -56,8 +73,8 @@ class ObservablesTests {
|
||||
observable.skip(1).first().subscribe { secondEvent.set(it to isInDatabaseTransaction()) }
|
||||
|
||||
databaseTransaction(database) {
|
||||
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||
val delayedSubject = source.bufferUntilDatabaseCommit()
|
||||
assertThat(source).isNotEqualTo(delayedSubject)
|
||||
delayedSubject.onNext(0)
|
||||
assertThat(firstEvent.isDone).isFalse()
|
||||
assertThat(secondEvent.isDone).isFalse()
|
||||
@ -67,33 +84,31 @@ class ObservablesTests {
|
||||
assertThat(secondEvent.isDone).isFalse()
|
||||
|
||||
databaseTransaction(database) {
|
||||
val delayedSubject = subject.bufferUntilDatabaseCommit()
|
||||
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||
val delayedSubject = source.bufferUntilDatabaseCommit()
|
||||
assertThat(source).isNotEqualTo(delayedSubject)
|
||||
delayedSubject.onNext(1)
|
||||
assertThat(secondEvent.isDone).isFalse()
|
||||
}
|
||||
assertThat(secondEvent.isDone).isTrue()
|
||||
assertThat(secondEvent.get()).isEqualTo(1 to false)
|
||||
|
||||
toBeClosed.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `tee correctly copies observations to multiple observers`() {
|
||||
|
||||
val subject1 = PublishSubject.create<Int>()
|
||||
val subject2 = PublishSubject.create<Int>()
|
||||
val subject3 = PublishSubject.create<Int>()
|
||||
val source1 = PublishSubject.create<Int>()
|
||||
val source2 = PublishSubject.create<Int>()
|
||||
val source3 = PublishSubject.create<Int>()
|
||||
|
||||
val event1 = SettableFuture.create<Int>()
|
||||
val event2 = SettableFuture.create<Int>()
|
||||
val event3 = SettableFuture.create<Int>()
|
||||
|
||||
subject1.subscribe { event1.set(it) }
|
||||
subject2.subscribe { event2.set(it) }
|
||||
subject3.subscribe { event3.set(it) }
|
||||
source1.subscribe { event1.set(it) }
|
||||
source2.subscribe { event2.set(it) }
|
||||
source3.subscribe { event3.set(it) }
|
||||
|
||||
val tee = subject1.tee(subject2, subject3)
|
||||
val tee = source1.tee(source2, source3)
|
||||
tee.onNext(0)
|
||||
|
||||
assertThat(event1.isDone).isTrue()
|
||||
@ -104,19 +119,19 @@ class ObservablesTests {
|
||||
assertThat(event3.get()).isEqualTo(0)
|
||||
|
||||
tee.onCompleted()
|
||||
assertThat(subject1.hasCompleted()).isTrue()
|
||||
assertThat(subject2.hasCompleted()).isTrue()
|
||||
assertThat(subject3.hasCompleted()).isTrue()
|
||||
assertThat(source1.hasCompleted()).isTrue()
|
||||
assertThat(source2.hasCompleted()).isTrue()
|
||||
assertThat(source3.hasCompleted()).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `combine tee and bufferUntilDatabaseCommit`() {
|
||||
val (toBeClosed, database) = configureDatabase(makeTestDataSourceProperties())
|
||||
val database = createDatabase()
|
||||
|
||||
val subject = PublishSubject.create<Int>()
|
||||
val source = PublishSubject.create<Int>()
|
||||
val teed = PublishSubject.create<Int>()
|
||||
|
||||
val observable: Observable<Int> = subject
|
||||
val observable: Observable<Int> = source
|
||||
|
||||
val firstEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
val teedEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
@ -126,8 +141,8 @@ class ObservablesTests {
|
||||
teed.first().subscribe { teedEvent.set(it to isInDatabaseTransaction()) }
|
||||
|
||||
databaseTransaction(database) {
|
||||
val delayedSubject = subject.bufferUntilDatabaseCommit().tee(teed)
|
||||
assertThat(subject).isNotEqualTo(delayedSubject)
|
||||
val delayedSubject = source.bufferUntilDatabaseCommit().tee(teed)
|
||||
assertThat(source).isNotEqualTo(delayedSubject)
|
||||
delayedSubject.onNext(0)
|
||||
assertThat(firstEvent.isDone).isFalse()
|
||||
assertThat(teedEvent.isDone).isTrue()
|
||||
@ -136,7 +151,90 @@ class ObservablesTests {
|
||||
|
||||
assertThat(firstEvent.get()).isEqualTo(0 to false)
|
||||
assertThat(teedEvent.get()).isEqualTo(0 to true)
|
||||
}
|
||||
|
||||
toBeClosed.close()
|
||||
@Test
|
||||
fun `new transaction open in observer when wrapped`() {
|
||||
val database = createDatabase()
|
||||
|
||||
val source = PublishSubject.create<Int>()
|
||||
val observableWithDbTx: Observable<Int> = source.wrapWithDatabaseTransaction()
|
||||
|
||||
val undelayedEvent = SettableFuture.create<Pair<Int, Boolean>>()
|
||||
val delayedEventFromSecondObserver = SettableFuture.create<Pair<Int, UUID?>>()
|
||||
val delayedEventFromThirdObserver = SettableFuture.create<Pair<Int, UUID?>>()
|
||||
|
||||
observableWithDbTx.first().subscribe { undelayedEvent.set(it to isInDatabaseTransaction()) }
|
||||
|
||||
fun observeSecondEvent(event: Int, future: SettableFuture<Pair<Int, UUID?>>) {
|
||||
future.set(event to if (isInDatabaseTransaction()) StrandLocalTransactionManager.transactionId else null)
|
||||
}
|
||||
|
||||
observableWithDbTx.skip(1).first().subscribe { observeSecondEvent(it, delayedEventFromSecondObserver) }
|
||||
observableWithDbTx.skip(1).first().subscribe { observeSecondEvent(it, delayedEventFromThirdObserver) }
|
||||
|
||||
databaseTransaction(database) {
|
||||
val commitDelayedSource = source.bufferUntilDatabaseCommit()
|
||||
assertThat(source).isNotEqualTo(commitDelayedSource)
|
||||
commitDelayedSource.onNext(0)
|
||||
source.onNext(1)
|
||||
assertThat(undelayedEvent.isDone).isTrue()
|
||||
assertThat(undelayedEvent.get()).isEqualTo(1 to true)
|
||||
assertThat(delayedEventFromSecondObserver.isDone).isFalse()
|
||||
}
|
||||
assertThat(delayedEventFromSecondObserver.isDone).isTrue()
|
||||
|
||||
assertThat(delayedEventFromSecondObserver.get().first).isEqualTo(0)
|
||||
assertThat(delayedEventFromSecondObserver.get().second).isNotNull()
|
||||
assertThat(delayedEventFromThirdObserver.get().first).isEqualTo(0)
|
||||
assertThat(delayedEventFromThirdObserver.get().second).isNotNull()
|
||||
|
||||
// Test that the two observers of the second event were notified inside the same database transaction.
|
||||
assertThat(delayedEventFromSecondObserver.get().second).isEqualTo(delayedEventFromThirdObserver.get().second)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check wrapping in db tx doesn't eagerly subscribe`() {
|
||||
val database = createDatabase()
|
||||
|
||||
val source = PublishSubject.create<Int>()
|
||||
var subscribed = false
|
||||
val event = SettableFuture.create<Int>()
|
||||
|
||||
val bufferedObservable: Observable<Int> = source.bufferUntilSubscribed().doOnSubscribe { subscribed = true }
|
||||
val databaseWrappedObservable: Observable<Int> = bufferedObservable.wrapWithDatabaseTransaction(database)
|
||||
|
||||
source.onNext(0)
|
||||
|
||||
assertThat(subscribed).isFalse()
|
||||
assertThat(event.isDone).isFalse()
|
||||
|
||||
databaseWrappedObservable.first().subscribe { event.set(it) }
|
||||
source.onNext(1)
|
||||
|
||||
assertThat(event.isDone).isTrue()
|
||||
assertThat(event.get()).isEqualTo(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check wrapping in db tx unsubscribes`() {
|
||||
val database = createDatabase()
|
||||
|
||||
val source = PublishSubject.create<Int>()
|
||||
var unsubscribed = false
|
||||
|
||||
val bufferedObservable: Observable<Int> = source.bufferUntilSubscribed().doOnUnsubscribe { unsubscribed = true }
|
||||
val databaseWrappedObservable: Observable<Int> = bufferedObservable.wrapWithDatabaseTransaction(database)
|
||||
|
||||
assertThat(unsubscribed).isFalse()
|
||||
|
||||
val subscription1 = databaseWrappedObservable.subscribe { }
|
||||
val subscription2 = databaseWrappedObservable.subscribe { }
|
||||
|
||||
subscription1.unsubscribe()
|
||||
assertThat(unsubscribed).isFalse()
|
||||
|
||||
subscription2.unsubscribe()
|
||||
assertThat(unsubscribed).isTrue()
|
||||
}
|
||||
}
|
@ -8,11 +8,10 @@ import net.corda.core.crypto.X509Utilities
|
||||
import net.corda.core.div
|
||||
import net.corda.core.exists
|
||||
import net.corda.core.readLines
|
||||
import net.corda.node.services.config.NodeConfiguration
|
||||
import net.corda.testing.TestNodeConfiguration
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.rules.TemporaryFolder
|
||||
import java.nio.file.Path
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertTrue
|
||||
@ -20,11 +19,10 @@ import kotlin.test.assertTrue
|
||||
class CertificateSignerTest {
|
||||
@Rule
|
||||
@JvmField
|
||||
val tempFolder: TemporaryFolder = TemporaryFolder()
|
||||
val tempFolder = TemporaryFolder()
|
||||
|
||||
@Test
|
||||
fun buildKeyStore() {
|
||||
|
||||
val id = SecureHash.randomSHA256().toString()
|
||||
|
||||
val certs = arrayOf(X509Utilities.createSelfSignedCACert("CORDA_CLIENT_CA").certificate,
|
||||
@ -36,27 +34,20 @@ class CertificateSignerTest {
|
||||
on { retrieveCertificates(eq(id)) }.then { certs }
|
||||
}
|
||||
|
||||
val config = TestNodeConfiguration(
|
||||
baseDirectory = tempFolder.root.toPath(),
|
||||
myLegalName = "me",
|
||||
networkMapService = null)
|
||||
|
||||
val config = object : NodeConfiguration {
|
||||
override val basedir: Path = tempFolder.root.toPath()
|
||||
override val myLegalName: String = "me"
|
||||
override val nearestCity: String = "London"
|
||||
override val emailAddress: String = ""
|
||||
override val devMode: Boolean = true
|
||||
override val exportJMXto: String = ""
|
||||
override val keyStorePassword: String = "testpass"
|
||||
override val trustStorePassword: String = "trustpass"
|
||||
}
|
||||
|
||||
assertFalse(config.keyStorePath.exists())
|
||||
assertFalse(config.trustStorePath.exists())
|
||||
assertFalse(config.keyStoreFile.exists())
|
||||
assertFalse(config.trustStoreFile.exists())
|
||||
|
||||
CertificateSigner(config, certService).buildKeyStore()
|
||||
|
||||
assertTrue(config.keyStorePath.exists())
|
||||
assertTrue(config.trustStorePath.exists())
|
||||
assertTrue(config.keyStoreFile.exists())
|
||||
assertTrue(config.trustStoreFile.exists())
|
||||
|
||||
X509Utilities.loadKeyStore(config.keyStorePath, config.keyStorePassword).run {
|
||||
X509Utilities.loadKeyStore(config.keyStoreFile, config.keyStorePassword).run {
|
||||
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
|
||||
assertTrue(containsAlias(X509Utilities.CORDA_CLIENT_CA))
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||
@ -65,7 +56,7 @@ class CertificateSignerTest {
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY))
|
||||
}
|
||||
|
||||
X509Utilities.loadKeyStore(config.trustStorePath, config.trustStorePassword).run {
|
||||
X509Utilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword).run {
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY))
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_CLIENT_CA))
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_INTERMEDIATE_CA))
|
||||
@ -74,7 +65,6 @@ class CertificateSignerTest {
|
||||
assertFalse(containsAlias(X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY))
|
||||
}
|
||||
|
||||
assertEquals(id, (config.certificatesPath / "certificate-request-id.txt").readLines { it.findFirst().get() })
|
||||
assertEquals(id, (config.certificatesDirectory / "certificate-request-id.txt").readLines { it.findFirst().get() })
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user