Merge remote-tracking branch 'corda-public/master'

This commit is contained in:
Chris Rankin
2017-01-26 10:16:47 +00:00
9095 changed files with 444278 additions and 150156 deletions

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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()))
)))
}

View File

@ -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)
}
}

View File

@ -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))
}
}
}

View File

@ -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()
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}

View 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)
}
}

View File

@ -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())
}

View File

@ -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()))
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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>> {

View File

@ -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)

View File

@ -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)

View File

@ -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>

View File

@ -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)
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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) }
}
}

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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) {

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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())
}
}

View File

@ -58,7 +58,7 @@ class DBTransactionMappingStorage : StateMachineRecordedTransactionMappingStorag
mutex.locked {
return Pair(
stateMachineTransactionMap.map { StateMachineTransactionMapping(it.value, it.key) },
updates.bufferUntilSubscribed()
updates.bufferUntilSubscribed().wrapWithDatabaseTransaction()
)
}
}

View File

@ -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())
}
}

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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})")
}
}

View File

@ -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<*>
}
}

View File

@ -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()
}

View File

@ -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 {

View File

@ -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>)

View File

@ -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<*, *, *>>()

View File

@ -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>() {

View File

@ -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))
}

View File

@ -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()
}

View File

@ -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" = ""
}

View 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")
}
}

View 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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)))
}
}

View File

@ -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) {

View File

@ -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)
}

View File

@ -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>()

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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() })
}
}