Merge remote-tracking branch 'remotes/open/master' into parkri-os-merge-201806-15-1

This commit is contained in:
rick.parker 2018-06-15 17:06:09 +01:00
commit 3a390e3643
52 changed files with 157 additions and 1941 deletions

View File

@ -71,7 +71,7 @@ buildscript {
ext.h2_version = '1.4.197' // Update docs if renamed or removed.
ext.postgresql_version = '42.1.4'
ext.rxjava_version = '1.2.4'
ext.dokka_version = '0.9.16-eap-2'
ext.dokka_version = '0.9.17'
ext.eddsa_version = '0.3.0' // Performance tuned version for enterprise.
ext.dependency_checker_version = '3.1.0'
ext.commons_collections_version = '4.1'
@ -101,8 +101,9 @@ buildscript {
ext.deterministic_rt_version = '1.0-SNAPSHOT'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131'
// Update 121 is required for ObjectInputFilter.
// Updates [131, 161] also have zip compression bugs on MacOS (High Sierra).
ext.java8_minUpdateVersion = '171'
repositories {
mavenLocal()
@ -210,6 +211,8 @@ allprojects {
}
tasks.withType(Test) {
failFast = true
// Prevent the project from creating temporary files outside of the build directory.
systemProperty 'java.io.tmpdir', buildDir.absolutePath
@ -491,6 +494,6 @@ if(file('corda-docs-only-build').exists() || (System.getenv('CORDA_DOCS_ONLY_BUI
}
wrapper {
gradleVersion = "4.4.1"
gradleVersion = "4.8"
distributionType = Wrapper.DistributionType.ALL
}

View File

@ -88,8 +88,7 @@ class DummyJar(
jar.write(arrayOfJunk(DATA_SIZE))
// One uncompressed text file
val text = """
Jar: ${_path.toAbsolutePath()}
val text = """Jar: ${_path.toAbsolutePath()}
Class: ${testClass.name}
""".toByteArray()
jar.putNextEntry(uncompressed("comment.txt", text))

View File

@ -262,7 +262,7 @@ task jarFilter(type: JarFilterTask) {
testProjectDir.newFile("build.gradle").writeText(script)
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getBasicArgsForTasks("jarFilter", "--stacktrace"))
.withArguments(getBasicArgsForTasks("jarFilter"))
.withPluginClasspath()
}

View File

@ -69,7 +69,7 @@ task metafix(type: MetaFixerTask) {
testProjectDir.newFile("build.gradle").writeText(script)
return GradleRunner.create()
.withProjectDir(testProjectDir.root)
.withArguments(getBasicArgsForTasks("metafix", "--stacktrace"))
.withArguments(getBasicArgsForTasks("metafix"))
.withPluginClasspath()
}

View File

@ -29,7 +29,7 @@ private val testGradleUserHome: String get() = testGradleUserHomeValue
?: throw AssumptionViolatedException("System property 'test.gradle.user.home' not set.")
fun getGradleArgsForTasks(vararg taskNames: String): MutableList<String> = getBasicArgsForTasks(*taskNames).apply { add("--info") }
fun getBasicArgsForTasks(vararg taskNames: String): MutableList<String> = mutableListOf(*taskNames, "-g", testGradleUserHome)
fun getBasicArgsForTasks(vararg taskNames: String): MutableList<String> = mutableListOf(*taskNames, "--stacktrace", "-g", testGradleUserHome)
@Throws(IOException::class)
fun copyResourceTo(resourceName: String, target: Path) {

View File

@ -18,16 +18,17 @@ import net.corda.core.context.Trace
import net.corda.core.crypto.random63BitValue
import net.corda.core.internal.logElapsedTime
import net.corda.core.internal.uncheckedCast
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.core.messaging.RPCOps
import net.corda.core.serialization.SerializationContext
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.internal.nodeSerializationEnv
import net.corda.core.utilities.*
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.contextLogger
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransport
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcConnectorTcpTransportsFromList
import net.corda.nodeapi.ArtemisTcpTransport.Companion.rpcInternalClientTcpTransport
import net.corda.nodeapi.RPCApi
import net.corda.core.messaging.ClientRpcSslOptions
import net.corda.nodeapi.internal.config.SSLConfiguration
import org.apache.activemq.artemis.api.core.SimpleString
import org.apache.activemq.artemis.api.core.TransportConfiguration
@ -84,6 +85,8 @@ class RPCClient<I : RPCOps>(
} else {
ActiveMQClient.createServerLocatorWithoutHA(*haPoolTransportConfigurations.toTypedArray())
}).apply {
connectionTTL = 60000
clientFailureCheckPeriod = 30000
retryInterval = rpcConfiguration.connectionRetryInterval.toMillis()
retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier
maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis()

View File

@ -16,7 +16,7 @@ proguardVersion=6.0.3
bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1
jsr305Version=3.0.2
artifactoryPluginVersion=4.4.18
artifactoryPluginVersion=4.7.3
snakeYamlVersion=1.19
caffeineVersion=2.6.2
metricsVersion=3.2.5

View File

@ -1,21 +1,27 @@
Building CorDapps against Master
================================
Building CorDapps against a non-release branch
==============================================
It is advisable to develop CorDapps against the most recent Corda stable release. However, you may need to build a CorDapp
against the unstable Master branch if your CorDapp uses a very recent feature, or you are using the CorDapp to test a PR
against an unstable non-release branch if your CorDapp uses a very recent feature, or you are using the CorDapp to test a PR
on the main codebase.
To work against the Master branch, proceed as follows:
To work against a non-release branch, proceed as follows:
1. Clone the `Corda repository <https://github.com/corda/corda>`_
2. Open a terminal window in the folder where you cloned the Corda repository
2. Check out the branch or commit of the Corda repository you want to work against
3. Use the following command to check out the latest master branch:
3. Make a note of the ``gradlePluginsVersion`` in the root ``constants.properties`` file of the Corda repository
4. Clone the `Corda Gradle Plugins repository <https://github.com/corda/corda-gradle-plugins>`_
``git checkout master; git pull``
5. Check out the tag of the Corda Gradle Plugins repository corresponding to the ``gradlePluginsVersion``
4. Publish Corda to your local Maven repository using the following commands:
6. Follow the instructions in the readme of the Corda Gradle Plugins repository to install this version of the Corda Gradle plugins locally
7. Open a terminal window in the folder where you cloned the Corda repository
8. Publish Corda to your local Maven repository using the following commands:
* Unix/Mac OSX: ``./gradlew install``
* Windows: ``gradlew.bat install``
@ -23,9 +29,13 @@ To work against the Master branch, proceed as follows:
.. warning:: If you do modify your local Corda repository after having published it to Maven local, then you must
re-publish it to Maven local for the local installation to reflect the changes you have made.
.. warning:: As the Corda repository evolves on a daily basis, two clones of the Master branch at different points in
time may differ. If you are using a Master release and need help debugging an error, then please let us know the
.. warning:: As the Corda repository evolves on a daily basis, two clones of an unstable branch at different points in
time may differ. If you are using an unstable release and need help debugging an error, then please let us know the
**commit** you are working from. This will help us ascertain the issue.
9. Make a note of the ``corda_release_version`` in the root ``build.gradle`` file of the Corda repository
5. Update the ``ext.corda_release_version`` property in your CorDapp's root ``build.gradle`` file to match the version
here: https://github.com/corda/corda/blob/master/build.gradle#L7
10. In your CorDapp's root ``build.gradle`` file:
* Update ``ext.corda_release_version`` to the ``corda_release_version`` noted down earlier
* Update ``corda_gradle_plugins_version`` to the ``gradlePluginsVersion`` noted down earlier

Binary file not shown.

View File

@ -10,6 +10,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.8-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.4.1-all.zip

View File

@ -51,7 +51,7 @@ class ArtemisMessagingClient(
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted.
connectionTTL = 60000
clientFailureCheckPeriod = -1
clientFailureCheckPeriod = 30000
minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null
confirmationWindowSize = this@ArtemisMessagingClient.confirmationWindowSize

View File

@ -19,13 +19,7 @@ import net.corda.node.internal.Node
import net.corda.node.internal.StartedNode
import net.corda.testing.internal.IntegrationTestSchemas
import net.corda.testing.internal.toDatabaseSchemaName
import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.BOB_NAME
import net.corda.testing.core.CHARLIE_NAME
import net.corda.testing.core.DUMMY_NOTARY_NAME
import net.corda.testing.core.TestIdentity
import net.corda.testing.core.getTestPartyAndCertificate
import net.corda.testing.core.singleIdentity
import net.corda.testing.core.*
import net.corda.testing.node.internal.NodeBasedTest
import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatExceptionOfType

View File

@ -212,7 +212,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
/** Set to non-null once [start] has been successfully called. */
open val started get() = _started
@Volatile private var _started: StartedNode<AbstractNode>? = null
@Volatile
private var _started: StartedNode<AbstractNode>? = null
/** The implementation of the [CordaRPCOps] interface used by this node. */
open fun makeRPCOps(flowStarter: FlowStarter, smm: StateMachineManager): CordaRPCOps {
@ -348,6 +349,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val smm = makeStateMachineManager(database)
val flowLogicRefFactory = FlowLogicRefFactoryImpl(cordappLoader.appClassLoader)
val flowStarter = FlowStarterImpl(smm, flowLogicRefFactory)
val cordaServices = installCordaServices(flowStarter)
val schedulerService = NodeSchedulerService(
platformClock,
database,
@ -357,8 +359,20 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
flowLogicRefFactory = flowLogicRefFactory,
drainingModePollPeriod = configuration.drainingModePollPeriod,
nodeProperties = nodeProperties)
runOnStop += { schedulerService.join() }
tokenizableServices = nodeServices + cordaServices + schedulerService
try {
verifyCheckpointsCompatible(checkpointStorage, cordappProvider.cordapps, versionInfo.platformVersion, _services, tokenizableServices)
} catch (e: CheckpointIncompatibleException) {
if (configuration.devMode) {
Node.printWarning(e.message)
} else {
throw e
}
}
(serverThread as? ExecutorService)?.let {
runOnStop += {
// We wait here, even though any in-flight messages should have been drained away because the
@ -372,8 +386,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
val rpcOps = makeRPCOps(flowStarter, smm)
startMessagingService(rpcOps)
installCoreFlows()
val cordaServices = installCordaServices(flowStarter)
tokenizableServices = nodeServices + cordaServices + schedulerService
registerCordappFlows(smm)
_services.rpcFlows += cordappLoader.cordapps.flatMap { it.rpcFlows }
startShell()
@ -441,7 +454,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
serial = 0
)
val nodeInfoFromDb = networkMapCache.getNodeByLegalName(identity.name)
val nodeInfoFromDb = getPreviousNodeInfoIfPresent(networkMapCache, identity)
val nodeInfo = if (potentialNodeInfo == nodeInfoFromDb?.copy(serial = 0)) {
// The node info hasn't changed. We use the one from the database to preserve the serial.
@ -471,6 +485,19 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
return Pair(keyPairs, nodeInfo)
}
private fun getPreviousNodeInfoIfPresent(networkMapCache: NetworkMapCacheBaseInternal, identity: PartyAndCertificate): NodeInfo? {
val nodeInfosFromDb = networkMapCache.getNodesByLegalName(identity.name)
return when (nodeInfosFromDb.size) {
0 -> null
1 -> nodeInfosFromDb[0]
else -> {
log.warn("Found more than one node registration with our legal name, this is only expected if our keypair has been regenerated")
nodeInfosFromDb[0]
}
}
}
// Publish node info on startup and start task that sends every day a heartbeat - republishes node info.
private fun tryPublishNodeInfoAsync(signedNodeInfo: SignedNodeInfo, networkMapClient: NetworkMapClient) {
// By default heartbeat interval should be set to 1 day, but for testing we may change it.
@ -733,15 +760,6 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
networkParameters: NetworkParameters): MutableList<Any> {
checkpointStorage = DBCheckpointStorage()
try {
verifyCheckpointsCompatible(checkpointStorage, cordappProvider.cordapps, versionInfo.platformVersion)
} catch (e: CheckpointIncompatibleException) {
if (configuration.devMode) {
Node.printWarning(e.message)
} else {
throw e
}
}
val keyManagementService = makeKeyManagementService(identityService, keyPairs, database)
_services = ServiceHubInternalImpl(
@ -757,7 +775,9 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
nodeProperties,
networkParameters,
servicesForResolution)
network = makeMessagingService(database, nodeInfo, nodeProperties, networkParameters)
return mutableListOf(attachments, network, services.vaultService,
services.keyManagementService, services.identityService, platformClock,
services.auditService, services.monitoringService, services.networkMapCache, services.schemaService,
@ -847,7 +867,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
}
private fun makeCoreNotaryService(notaryConfig: NotaryConfig, database: CordaPersistence): NotaryService {
val notaryKey = myNotaryIdentity?.owningKey ?: throw IllegalArgumentException("No notary identity initialized when creating a notary service")
val notaryKey = myNotaryIdentity?.owningKey
?: throw IllegalArgumentException("No notary identity initialized when creating a notary service")
return notaryConfig.run {
when {
raft != null -> {
@ -946,7 +967,7 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
throw ConfigurationException("The name '$singleName' for $id doesn't match what's in the key store: $subject")
} else if (notaryConfig != null && notaryConfig.isClusterConfig && notaryConfig.serviceLegalName != null && subject != notaryConfig.serviceLegalName) {
// Note that we're not checking if `notaryConfig.serviceLegalName` is not present for backwards compatibility.
throw ConfigurationException("The name of the notary service '${notaryConfig.serviceLegalName}' for $id doesn't match what's in the key store: $subject. "+
throw ConfigurationException("The name of the notary service '${notaryConfig.serviceLegalName}' for $id doesn't match what's in the key store: $subject. " +
"You might need to adjust the configuration of `notary.serviceLegalName`.")
}
@ -968,8 +989,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
log.info("Starting Jolokia agent on HTTP port: $port")
val libDir = Paths.get(configuration.baseDirectory.toString(), "drivers")
val jarFilePath = JVMAgentRegistry.resolveAgentJar(
"jolokia-jvm-${NodeBuildProperties.JOLOKIA_AGENT_VERSION}-agent.jar", libDir) ?:
throw Error("Unable to locate agent jar file")
"jolokia-jvm-${NodeBuildProperties.JOLOKIA_AGENT_VERSION}-agent.jar", libDir)
?: throw Error("Unable to locate agent jar file")
log.info("Agent jar file: $jarFilePath")
JVMAgentRegistry.attach("jolokia", "port=$port", jarFilePath)
}
@ -1005,7 +1026,8 @@ abstract class AbstractNode(val configuration: NodeConfiguration,
override val networkMapUpdater: NetworkMapUpdater get() = this@AbstractNode.networkMapUpdater
override fun <T : SerializeAsToken> cordaService(type: Class<T>): T {
require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" }
return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist")
return cordappServices.getInstance(type)
?: throw IllegalArgumentException("Corda service ${type.name} does not exist")
}
override fun getFlowFactory(initiatingFlowClass: Class<out FlowLogic<*>>): InitiatedFlowFactory<*>? {

View File

@ -3,11 +3,14 @@ package net.corda.node.internal
import net.corda.core.cordapp.Cordapp
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogic
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SerializationDefaults
import net.corda.core.serialization.deserialize
import net.corda.node.services.api.CheckpointStorage
import net.corda.node.services.statemachine.SubFlow
import net.corda.node.services.statemachine.SubFlowVersion
import net.corda.serialization.internal.SerializeAsTokenContextImpl
import net.corda.serialization.internal.withTokenContext
object CheckpointVerifier {
@ -15,10 +18,14 @@ object CheckpointVerifier {
* Verifies that all Checkpoints stored in the db can be safely loaded with the currently installed version.
* @throws CheckpointIncompatibleException if any offending checkpoint is found.
*/
fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int) {
fun verifyCheckpointsCompatible(checkpointStorage: CheckpointStorage, currentCordapps: List<Cordapp>, platformVersion: Int, serviceHub: ServiceHub, tokenizableServices: List<Any>) {
val checkpointSerializationContext = SerializationDefaults.CHECKPOINT_CONTEXT.withTokenContext(
SerializeAsTokenContextImpl(tokenizableServices, SerializationDefaults.SERIALIZATION_FACTORY, SerializationDefaults.CHECKPOINT_CONTEXT, serviceHub)
)
checkpointStorage.getAllCheckpoints().forEach { (_, serializedCheckpoint) ->
val checkpoint = try {
serializedCheckpoint.deserialize(context = SerializationDefaults.CHECKPOINT_CONTEXT)
serializedCheckpoint.deserialize(context = checkpointSerializationContext)
} catch (e: Exception) {
throw CheckpointIncompatibleException.CannotBeDeserialisedException(e)
}

View File

@ -27,7 +27,7 @@ class InternalRPCMessagingClient(val sslConfig: SSLConfiguration, val serverAddr
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted.
connectionTTL = 60000
clientFailureCheckPeriod = -1
clientFailureCheckPeriod = 30000
minLargeMessageSize = maxMessageSize
isUseGlobalPools = nodeSerializationEnv != null
}

View File

@ -156,7 +156,7 @@ class P2PMessagingClient(val config: NodeConfiguration,
// Never time out on our loopback Artemis connections. If we switch back to using the InVM transport this
// would be the default and the two lines below can be deleted.
connectionTTL = 60000
clientFailureCheckPeriod = -1
clientFailureCheckPeriod = 30000
minLargeMessageSize = maxMessageSize + JOURNAL_HEADER_SIZE
isUseGlobalPools = nodeSerializationEnv != null
confirmationWindowSize = config.enterpriseConfiguration.tuning.p2pConfirmationWindowSize

View File

@ -177,7 +177,7 @@ open class PersistentNetworkMapCache(
}
}
override fun getNodesByLegalName(name: CordaX500Name): List<NodeInfo> = database.transaction { queryByLegalName(session, name) }
override fun getNodesByLegalName(name: CordaX500Name): List<NodeInfo> = database.transaction { queryByLegalName(session, name) }.sortedByDescending { it.serial }
override fun getNodesByLegalIdentityKey(identityKey: PublicKey): List<NodeInfo> = nodesByKeyCache[identityKey]!!

View File

@ -68,7 +68,7 @@ class NodeTest {
@Test
fun `generateAndSaveNodeInfo works`() {
val configuration = createConfig()
val configuration = createConfig(ALICE_NAME)
val info = VersionInfo(789, "3.0", "SNAPSHOT", "R3")
configureDatabase(configuration.dataSourceProperties, configuration.database, { null }, { null }).use { database ->
val node = Node(configuration, info, initialiseSerialization = false)
@ -78,7 +78,7 @@ class NodeTest {
@Test
fun `clear network map cache works`() {
val configuration = createConfig()
val configuration = createConfig(ALICE_NAME)
val (nodeInfo, _) = createNodeInfoAndSigned(ALICE_NAME)
configureDatabase(configuration.dataSourceProperties, configuration.database, { null }, { null }).use {
it.transaction {
@ -103,6 +103,52 @@ class NodeTest {
}
}
@Test
fun `Node can start with multiple keypairs for it's identity`() {
val configuration = createConfig(ALICE_NAME)
val (nodeInfo1, _) = createNodeInfoAndSigned(ALICE_NAME)
val (nodeInfo2, _) = createNodeInfoAndSigned(ALICE_NAME)
val persistentNodeInfo2 = NodeInfoSchemaV1.PersistentNodeInfo(
id = 0,
hash = nodeInfo2.serialize().hash.toString(),
addresses = nodeInfo2.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) },
legalIdentitiesAndCerts = nodeInfo2.legalIdentitiesAndCerts.mapIndexed { idx, elem ->
NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0)
},
platformVersion = nodeInfo2.platformVersion,
serial = nodeInfo2.serial
)
val persistentNodeInfo1 = NodeInfoSchemaV1.PersistentNodeInfo(
id = 0,
hash = nodeInfo1.serialize().hash.toString(),
addresses = nodeInfo1.addresses.map { NodeInfoSchemaV1.DBHostAndPort.fromHostAndPort(it) },
legalIdentitiesAndCerts = nodeInfo1.legalIdentitiesAndCerts.mapIndexed { idx, elem ->
NodeInfoSchemaV1.DBPartyAndCertificate(elem, isMain = idx == 0)
},
platformVersion = nodeInfo1.platformVersion,
serial = nodeInfo1.serial
)
configureDatabase(configuration.dataSourceProperties, configuration.database, { null }, { null }).use {
it.transaction {
session.save(persistentNodeInfo1)
}
it.transaction {
session.save(persistentNodeInfo2)
}
val node = Node(configuration, rigorousMock<VersionInfo>().also {
doReturn(10).whenever(it).platformVersion
}, initialiseSerialization = false)
//this throws an exception with old behaviour
node.generateNodeInfo()
}
}
private fun getAllInfos(database: CordaPersistence): List<NodeInfoSchemaV1.PersistentNodeInfo> {
return database.transaction {
val criteria = session.criteriaBuilder.createQuery(NodeInfoSchemaV1.PersistentNodeInfo::class.java)
@ -111,11 +157,10 @@ class NodeTest {
}
}
private fun createConfig(): NodeConfiguration {
private fun createConfig(nodeName: CordaX500Name): NodeConfiguration {
val dataSourceProperties = makeTestDataSourceProperties()
val databaseConfig = DatabaseConfig()
val nodeAddress = NetworkHostAndPort("0.1.2.3", 456)
val nodeName = CordaX500Name("Manx Blockchain Corp", "Douglas", "IM")
return rigorousMock<AbstractNodeConfiguration>().also {
doReturn(null).whenever(it).relay
doReturn(nodeAddress).whenever(it).p2pAddress

View File

@ -30,6 +30,7 @@ import net.corda.testing.core.ALICE_NAME
import net.corda.testing.core.SerializationEnvironmentRule
import net.corda.testing.core.TestIdentity
import net.corda.testing.internal.LogHelper
import net.corda.testing.node.MockServices
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import org.assertj.core.api.Assertions
import org.assertj.core.api.Assertions.assertThat
@ -165,13 +166,14 @@ class DBCheckpointStorageTests {
@Test
fun `verify checkpoints compatible`() {
val mockServices = MockServices(emptyList(), ALICE.name)
database.transaction {
val (id, checkpoint) = newCheckpoint(1)
checkpointStorage.addCheckpoint(id, checkpoint)
}
database.transaction {
CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1)
CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1, mockServices, emptyList())
}
database.transaction {
@ -181,7 +183,7 @@ class DBCheckpointStorageTests {
Assertions.assertThatThrownBy {
database.transaction {
CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1)
CheckpointVerifier.verifyCheckpointsCompatible(checkpointStorage, emptyList(), 1, mockServices, emptyList())
}
}.isInstanceOf(CheckpointIncompatibleException::class.java)
}

View File

@ -30,6 +30,7 @@ import org.junit.Test
import java.sql.SQLException
import java.time.Duration
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
@ -48,7 +49,7 @@ class RetryFlowMockTest {
RetryFlow.count = 0
SendAndRetryFlow.count = 0
RetryInsertFlow.count = 0
KeepSendingFlow.count = 0
KeepSendingFlow.count.set(0)
}
private fun <T> StartedNode<MockNode>.startFlow(logic: FlowLogic<T>): CordaFuture<T> {
@ -115,7 +116,7 @@ class RetryFlowMockTest {
}
})
// Now short circuit the iterations so the flow finishes soon.
KeepSendingFlow.count = count - 2
KeepSendingFlow.count.set(count - 2)
while (nodeA.smm.allStateMachines.size > 0) {
Thread.sleep(10)
}
@ -240,8 +241,7 @@ class ReceiveFlow2(private val other: FlowSession) : FlowLogic<Unit>() {
@InitiatingFlow
class KeepSendingFlow(private val i: Int, private val other: Party) : FlowLogic<Unit>() {
companion object {
@Volatile
var count = 0
val count = AtomicInteger(0)
}
@Suspendable
@ -251,7 +251,7 @@ class KeepSendingFlow(private val i: Int, private val other: Party) : FlowLogic<
do {
logger.info("Sending... $count")
session.send("Boo")
} while (count++ < i)
} while (count.getAndIncrement() < i)
}
}

View File

@ -1,13 +0,0 @@
# Network Visualiser
This package contains a network visualiser that uses a simulation to visualise the interaction and messages between nodes on the Corda network.
Please see the either the [online documentation](https://docs.corda.net/network-simulator.html) for more info on the network visualiser, or the [included offline version](../../docs/build/html/network-simulator.html).
From the root directory of the repository, run it like this (Windows):
gradle samples:network-visualiser:run
or (Mac / Unix)
./gradlew samples:network-visualiser:run

View File

@ -1,90 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
buildscript {
ext {
springBootVersion = '1.5.7.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath "org.springframework.boot:spring-boot-gradle-plugin:$springBootVersion"
classpath "io.spring.gradle:dependency-management-plugin:1.0.4.RELEASE"
}
}
// Spring Boot plugin adds a numerous hardcoded dependencies in the version much lower then Corda expects
// causing the problems in runtime. Those can be changed by manipulating above properties
// See https://github.com/spring-gradle-plugins/dependency-management-plugin/blob/master/README.md#changing-the-value-of-a-version-property
// This has to be repeated here as otherwise the order of files does matter
// See a list here: https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-dependencies/pom.xml
ext['artemis.version'] = "$artemis_version"
ext['hibernate.version'] = "$hibernate_version"
ext['jackson.version'] = "$jackson_version"
ext['dropwizard-metrics.version'] = "$metrics_version"
ext['mockito.version'] = "$mockito_version"
apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'idea'
apply plugin: 'application'
apply plugin: 'org.springframework.boot'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'us.kirchmeier.capsule'
// Spring Boot plugin have to be reimported, however it picks up the settings from irs-demo, so there is no need to
// reconfigure
// Warning: The network visualiser is not a Cordapp so please do not use it as an example of how
// to build a cordapp
dependencies {
testCompile "junit:junit:$junit_version"
// Corda integration dependencies
compile project(path: ":node:capsule", configuration: 'runtimeArtifacts')
compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts')
compile project(':core')
compile project(':finance')
compile project(':node-driver')
compile project(':finance')
compile project(':samples:irs-demo')
// GraphStream: For visualisation
compileOnly "co.paralleluniverse:capsule:$capsule_version"
}
idea {
module {
downloadJavadoc = true // defaults to false
downloadSources = true
}
}
mainClassName = 'net.corda.netmap.NetworkMapVisualiser'
task deployVisualiser(type: FatCapsule) {
applicationClass 'net.corda.netmap.NetworkMapVisualiser'
reallyExecutable
capsuleManifest {
minJavaVersion = '1.8.0'
javaAgents = [configurations.quasar.singleFile.name]
}
}
jar {
manifest {
attributes(
'Automatic-Module-Name': 'net.corda.samples.network.visualiser'
)
}
}

View File

@ -1,369 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap
import javafx.animation.*
import javafx.application.Application
import javafx.application.Platform
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.value.WritableValue
import javafx.geometry.Insets
import javafx.scene.input.KeyCode
import javafx.scene.input.KeyCodeCombination
import javafx.scene.layout.VBox
import javafx.stage.Stage
import javafx.util.Duration
import net.corda.core.serialization.deserialize
import net.corda.core.utilities.ProgressTracker
import net.corda.netmap.VisualiserViewModel.Style
import net.corda.netmap.simulation.IRSSimulation
import net.corda.node.services.statemachine.*
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.internal.InternalMockNetwork
import rx.Scheduler
import rx.schedulers.Schedulers
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.*
import kotlin.concurrent.schedule
import kotlin.concurrent.scheduleAtFixedRate
import kotlin.system.exitProcess
fun <T : Any> WritableValue<T>.keyValue(endValue: T, interpolator: Interpolator = Interpolator.EASE_OUT) = KeyValue(this, endValue, interpolator)
// TODO: This code is all horribly ugly. Refactor to use TornadoFX to clean it up.
class NetworkMapVisualiser : Application() {
enum class NodeType {
BANK, SERVICE
}
enum class RunPauseButtonLabel {
RUN, PAUSE;
override fun toString(): String {
return name.toLowerCase().capitalize()
}
}
sealed class RunningPausedState {
class Running(val tickTimer: TimerTask) : RunningPausedState()
class Paused : RunningPausedState()
val buttonLabel: RunPauseButtonLabel
get() {
return when (this) {
is RunningPausedState.Running -> RunPauseButtonLabel.PAUSE
is RunningPausedState.Paused -> RunPauseButtonLabel.RUN
}
}
}
private val view = VisualiserView()
private val viewModel = VisualiserViewModel()
val timer = Timer()
val uiThread: Scheduler = Schedulers.from { Platform.runLater(it) }
override fun start(stage: Stage) {
viewModel.view = view
viewModel.presentationMode = "--presentation-mode" in parameters.raw
buildScene(stage)
viewModel.displayStyle = if ("--circle" in parameters.raw) {
Style.CIRCLE
} else {
viewModel.displayStyle
}
val simulation = viewModel.simulation
// Update the white-backgrounded label indicating what flow step it's up to.
simulation.allFlowSteps.observeOn(uiThread).subscribe { (node, change) ->
val label = viewModel.nodesToWidgets[node]!!.statusLabel
if (change is ProgressTracker.Change.Position) {
// Fade in the status label if it's our first step.
if (label.text == "") {
with(FadeTransition(Duration(150.0), label)) {
fromValue = 0.0
toValue = 1.0
play()
}
}
label.text = change.newStep.label
if (change.newStep == ProgressTracker.DONE && change.tracker == change.tracker.topLevelTracker) {
runLater(500, -1) {
// Fade out the status label.
with(FadeTransition(Duration(750.0), label)) {
fromValue = 1.0
toValue = 0.0
setOnFinished { label.text = "" }
play()
}
}
}
} else if (change is ProgressTracker.Change.Rendering) {
label.text = change.ofStep.label
}
}
// Fire the message bullets between nodes.
simulation.mockNet.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer ->
val senderNode: InternalMockNetwork.MockNode = simulation.mockNet.addressToNode(msg.sender)
val destNode: InternalMockNetwork.MockNode = simulation.mockNet.addressToNode(msg.recipients)
if (transferIsInteresting(msg)) {
viewModel.nodesToWidgets[senderNode]!!.pulseAnim.play()
viewModel.fireBulletBetweenNodes(senderNode, destNode, "bank", "bank")
}
}
// Pulse all parties in a trade when the trade completes
simulation.doneSteps.observeOn(uiThread).subscribe { nodes: Collection<InternalMockNetwork.MockNode> ->
nodes.forEach { viewModel.nodesToWidgets[it]!!.longPulseAnim.play() }
}
stage.setOnCloseRequest { exitProcess(0) }
//stage.isMaximized = true
stage.show()
}
fun runLater(startAfter: Int, delayBetween: Int, body: () -> Unit) {
if (delayBetween != -1) {
timer.scheduleAtFixedRate(startAfter.toLong(), delayBetween.toLong()) {
Platform.runLater {
body()
}
}
} else {
timer.schedule(startAfter.toLong()) {
Platform.runLater {
body()
}
}
}
}
private fun buildScene(stage: Stage) {
view.stage = stage
view.setup(viewModel.runningPausedState, viewModel.displayStyle, viewModel.presentationMode)
bindSidebar()
bindTopbar()
viewModel.createNodes()
// Spacebar advances simulation by one step.
stage.scene.accelerators[KeyCodeCombination(KeyCode.SPACE)] = Runnable { onNextInvoked() }
reloadStylesheet(stage)
stage.focusedProperty().addListener { _, _, new ->
if (new) {
reloadStylesheet(stage)
}
}
}
private fun bindTopbar() {
view.resetButton.setOnAction({ reset() })
view.nextButton.setOnAction {
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation()
} else {
onNextInvoked()
}
}
viewModel.simulation.networkInitialisationFinished.thenAccept {
view.simulateInitialisationCheckbox.isVisible = false
}
view.runPauseButton.setOnAction {
val oldRunningPausedState = viewModel.runningPausedState
val newRunningPausedState = when (oldRunningPausedState) {
is NetworkMapVisualiser.RunningPausedState.Running -> {
oldRunningPausedState.tickTimer.cancel()
view.nextButton.isDisable = false
view.resetButton.isDisable = false
NetworkMapVisualiser.RunningPausedState.Paused()
}
is NetworkMapVisualiser.RunningPausedState.Paused -> {
val tickTimer = timer.scheduleAtFixedRate(viewModel.stepDuration.toMillis().toLong(), viewModel.stepDuration.toMillis().toLong()) {
Platform.runLater {
onNextInvoked()
}
}
view.nextButton.isDisable = true
view.resetButton.isDisable = true
if (!view.simulateInitialisationCheckbox.isSelected && !viewModel.simulation.networkInitialisationFinished.isDone) {
skipNetworkInitialisation()
}
NetworkMapVisualiser.RunningPausedState.Running(tickTimer)
}
}
view.runPauseButton.text = newRunningPausedState.buttonLabel.toString()
viewModel.runningPausedState = newRunningPausedState
}
view.styleChoice.selectionModel.selectedItemProperty()
.addListener { _, _, newValue -> viewModel.displayStyle = newValue }
viewModel.simulation.dateChanges.observeOn(uiThread).subscribe { view.dateLabel.text = it.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM)) }
}
private fun reloadStylesheet(stage: Stage) {
stage.scene.stylesheets.clear()
stage.scene.stylesheets.add(NetworkMapVisualiser::class.java.getResource("styles.css").toString())
}
private fun bindSidebar() {
viewModel.simulation.allFlowSteps.observeOn(uiThread).subscribe { (node, change) ->
if (change is ProgressTracker.Change.Position) {
val tracker = change.tracker.topLevelTracker
if (change.newStep == ProgressTracker.DONE) {
if (change.tracker == tracker) {
// Flow done; schedule it for removal in a few seconds. We batch them up to make nicer
// animations.
updateProgressTrackerWidget(change)
println("Flow done for ${node.started!!.info.singleIdentity().name}")
viewModel.doneTrackers += tracker
} else {
// Subflow is done; ignore it.
}
} else if (!viewModel.trackerBoxes.containsKey(tracker)) {
// New flow started up; add.
val extraLabel = viewModel.simulation.extraNodeLabels[node]
val label = node.started!!.info.singleIdentity().name.organisation.let { if (extraLabel != null) "$it: $extraLabel" else it }
val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker)
println("Added: $tracker, $widget")
viewModel.trackerBoxes[tracker] = widget
view.sidebar.children += widget.vbox
} else {
updateProgressTrackerWidget(change)
}
} else if (change is ProgressTracker.Change.Structural) {
updateProgressTrackerWidget(change)
}
}
Timer().scheduleAtFixedRate(0, 500) {
Platform.runLater {
for (tracker in viewModel.doneTrackers) {
val pane = viewModel.trackerBoxes[tracker]!!.vbox
// Slide the other tracker widgets up and over this one.
val slideProp = SimpleDoubleProperty(0.0)
slideProp.addListener { _ -> pane.padding = Insets(0.0, 0.0, slideProp.value, 0.0) }
val timeline = Timeline(
KeyFrame(Duration(250.0),
KeyValue(pane.opacityProperty(), 0.0),
KeyValue(slideProp, -pane.height - 50.0) // Subtract the bottom padding gap.
)
)
timeline.setOnFinished {
println("Removed: $tracker")
val vbox = viewModel.trackerBoxes.remove(tracker)?.vbox
view.sidebar.children.remove(vbox)
}
timeline.play()
}
viewModel.doneTrackers.clear()
}
}
}
private fun updateProgressTrackerWidget(step: ProgressTracker.Change) {
if (step is ProgressTracker.Change.Position) {
// Animate the cursor to the right place.
Platform.runLater {
val tracker: ProgressTracker = step.tracker.topLevelTracker
val widget = viewModel.trackerBoxes[tracker] ?: return@runLater
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
// Figure out the index of the new step.
val curStep = allSteps.indexOfFirst { it.second == step.newStep }
with(TranslateTransition(Duration(350.0), widget.cursor)) {
fromY = widget.cursor.translateY
toY = (curStep * view.sideBarStepHeight) + (view.sideBarStepHeight / 2.0)
play()
}
}
} else if (step is ProgressTracker.Change.Structural) {
Platform.runLater {
val tracker: ProgressTracker = step.tracker.topLevelTracker
val widget = viewModel.trackerBoxes[tracker] ?: return@runLater
val new = view.buildProgressTrackerWidget(widget.label.text, tracker)
val prevWidget = viewModel.trackerBoxes[tracker]?.vbox ?: throw AssertionError("No previous widget for tracker: $tracker")
val i = (prevWidget.parent as VBox).children.indexOf(viewModel.trackerBoxes[tracker]?.vbox)
(prevWidget.parent as VBox).children[i] = new.vbox
viewModel.trackerBoxes[tracker] = new
}
}
}
var started = false
private fun startSimulation() {
if (!started) {
viewModel.simulation.start()
started = true
}
}
private fun reset() {
viewModel.simulation.stop()
viewModel.simulation = IRSSimulation(true, false, null)
started = false
start(view.stage)
}
private fun skipNetworkInitialisation() {
startSimulation()
while (!viewModel.simulation.networkInitialisationFinished.isDone) {
iterateSimulation()
}
}
private fun onNextInvoked() {
if (started) {
iterateSimulation()
} else {
startSimulation()
}
}
private fun iterateSimulation() {
// Loop until either we ran out of things to do, or we sent an interesting message.
while (true) {
val transfer: InMemoryMessagingNetwork.MessageTransfer = viewModel.simulation.iterate() ?: break
if (transferIsInteresting(transfer))
break
else
System.err.println("skipping boring $transfer")
}
}
private fun transferIsInteresting(transfer: InMemoryMessagingNetwork.MessageTransfer): Boolean {
// Loopback messages are boring.
if (transfer.sender == transfer.recipients) return false
val message = transfer.messageData.deserialize<SessionMessage>()
return when (message) {
is InitialSessionMessage -> message.firstPayload != null
is ExistingSessionMessage -> when (message.payload) {
is ConfirmSessionMessage -> false
is DataSessionMessage -> true
is ErrorSessionMessage -> true
is RejectSessionMessage -> true
is EndSessionMessage -> false
}
}
}
}
fun main(args: Array<String>) {
Application.launch(NetworkMapVisualiser::class.java, *args)
}

View File

@ -1,28 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap
import javafx.scene.paint.Color
internal
fun colorToRgb(color: Color): String {
val builder = StringBuilder()
builder.append("rgb(")
builder.append(Math.round(color.red * 256))
builder.append(",")
builder.append(Math.round(color.green * 256))
builder.append(",")
builder.append(Math.round(color.blue * 256))
builder.append(")")
return builder.toString()
}

View File

@ -1,315 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap
import javafx.animation.KeyFrame
import javafx.animation.Timeline
import javafx.application.Platform
import javafx.collections.FXCollections
import javafx.event.EventHandler
import javafx.geometry.Insets
import javafx.geometry.Pos
import javafx.scene.Group
import javafx.scene.Node
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import javafx.scene.input.ZoomEvent
import javafx.scene.layout.*
import javafx.scene.paint.Color
import javafx.scene.shape.Polygon
import javafx.scene.text.Font
import javafx.stage.Stage
import javafx.util.Duration
import net.corda.core.utilities.ProgressTracker
import net.corda.netmap.VisualiserViewModel.Style
data class TrackerWidget(val vbox: VBox, val cursorBox: Pane, val label: Label, val cursor: Polygon)
internal class VisualiserView {
lateinit var root: Pane
lateinit var stage: Stage
lateinit var splitter: SplitPane
lateinit var sidebar: VBox
lateinit var resetButton: Button
lateinit var nextButton: Button
lateinit var runPauseButton: Button
lateinit var simulateInitialisationCheckbox: CheckBox
lateinit var styleChoice: ChoiceBox<Style>
var dateLabel = Label("")
var scrollPane: ScrollPane? = null
var hideButton = Button("«").apply { styleClass += "hide-sidebar-button" }
// -23.2031,29.8406,33.0469,64.3209
val mapImage = ImageView(Image(NetworkMapVisualiser::class.java.getResourceAsStream("Europe.jpg")))
val backgroundColor: Color = mapImage.image.pixelReader.getColor(0, 0)
val stageWidth = 1024.0
val stageHeight = 768.0
var defaultZoom = 0.7
val bitmapWidth = 1900.0
val bitmapHeight = 1900.0
// This row height is controlled in the CSS and needs to match.
val sideBarStepHeight = 40.0
fun setup(runningPausedState: NetworkMapVisualiser.RunningPausedState,
displayStyle: Style,
presentationMode: Boolean) {
NetworkMapVisualiser::class.java.getResourceAsStream("SourceSansPro-Regular.otf").use {
Font.loadFont(it, 120.0)
}
if (displayStyle == Style.MAP) {
mapImage.onZoom = EventHandler<javafx.scene.input.ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
//repositionNodes()
}
}
scaleMap(displayStyle)
root = Pane(mapImage)
root.background = Background(BackgroundFill(backgroundColor, CornerRadii.EMPTY, Insets.EMPTY))
scrollPane = buildScrollPane(backgroundColor, displayStyle)
val vbox = makeTopBar(runningPausedState, displayStyle, presentationMode)
StackPane.setAlignment(vbox, Pos.TOP_CENTER)
// Now build the sidebar
val defaultSplitterPosition = 0.3
splitter = SplitPane(makeSidebar(), scrollPane)
splitter.styleClass += "splitter"
Platform.runLater {
splitter.dividers[0].position = defaultSplitterPosition
}
VBox.setVgrow(splitter, Priority.ALWAYS)
// And the left hide button.
hideButton = makeHideButton(defaultSplitterPosition)
val screenStack = VBox(vbox, StackPane(splitter, hideButton))
screenStack.styleClass += "root-pane"
stage.scene = Scene(screenStack, backgroundColor)
stage.width = 1024.0
stage.height = 768.0
}
fun buildScrollPane(backgroundColor: Color, displayStyle: Style): ScrollPane {
when (displayStyle) {
Style.MAP -> {
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
mapImage.onZoom = EventHandler<ZoomEvent> { event ->
event.consume()
mapImage.fitWidth = mapImage.fitWidth * event.zoomFactor
mapImage.fitHeight = mapImage.fitHeight * event.zoomFactor
}
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
return ScrollPane(Group(root)).apply {
when (displayStyle) {
Style.MAP -> {
hvalue = 0.4
vvalue = 0.7
}
Style.CIRCLE -> {
hvalue = 0.0
vvalue = 0.0
}
}
hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
isPannable = true
isFocusTraversable = false
style = "-fx-background-color: " + colorToRgb(backgroundColor)
styleClass += "edge-to-edge"
}
}
fun makeHideButton(defaultSplitterPosition: Double): Button {
var hideButtonToggled = false
hideButton.isFocusTraversable = false
hideButton.setOnAction {
if (!hideButtonToggled) {
hideButton.translateXProperty().unbind()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(0.0),
hideButton.translateXProperty().keyValue(0.0),
hideButton.rotateProperty().keyValue(180.0)
)
).play()
} else {
bindHideButtonPosition()
Timeline(
KeyFrame(Duration.millis(500.0),
splitter.dividers[0].positionProperty().keyValue(defaultSplitterPosition),
hideButton.rotateProperty().keyValue(0.0)
)
).play()
}
hideButtonToggled = !hideButtonToggled
}
bindHideButtonPosition()
StackPane.setAlignment(hideButton, Pos.TOP_LEFT)
return hideButton
}
fun bindHideButtonPosition() {
hideButton.translateXProperty().unbind()
hideButton.translateXProperty().bind(splitter.dividers[0].positionProperty().multiply(splitter.widthProperty()).subtract(hideButton.widthProperty()))
}
fun scaleMap(displayStyle: Style) {
when (displayStyle) {
Style.MAP -> {
mapImage.fitWidth = bitmapWidth * defaultZoom
mapImage.fitHeight = bitmapHeight * defaultZoom
}
Style.CIRCLE -> {
val scaleRatio = Math.min(stageWidth / bitmapWidth, stageHeight / bitmapHeight)
mapImage.fitWidth = bitmapWidth * scaleRatio
mapImage.fitHeight = bitmapHeight * scaleRatio
}
}
}
fun makeSidebar(): Node {
sidebar = VBox()
sidebar.styleClass += "sidebar"
sidebar.isFillWidth = true
val sp = ScrollPane(sidebar)
sp.isFitToWidth = true
sp.isFitToHeight = true
sp.styleClass += "sidebar"
sp.hbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.vbarPolicy = ScrollPane.ScrollBarPolicy.NEVER
sp.minWidth = 0.0
return sp
}
fun makeTopBar(runningPausedState: NetworkMapVisualiser.RunningPausedState,
displayStyle: Style,
presentationMode: Boolean): VBox {
nextButton = Button("Next").apply {
styleClass += "button"
styleClass += "next-button"
}
runPauseButton = Button(runningPausedState.buttonLabel.toString()).apply {
styleClass += "button"
styleClass += "run-button"
}
simulateInitialisationCheckbox = CheckBox("Simulate initialisation")
resetButton = Button("Reset").apply {
styleClass += "button"
styleClass += "reset-button"
}
val displayStyles = FXCollections.observableArrayList<Style>()
Style.values().forEach { displayStyles.add(it) }
styleChoice = ChoiceBox(displayStyles).apply {
styleClass += "choice"
styleClass += "style-choice"
}
styleChoice.value = displayStyle
val dropShadow = Pane().apply { styleClass += "drop-shadow-pane-horizontal"; minHeight = 8.0 }
val logoImage = ImageView(javaClass.getResource("Corda logo.png").toExternalForm())
logoImage.fitHeight = 65.0
logoImage.isPreserveRatio = true
val logoLabel = HBox(logoImage,
Label("Network Simulator").apply { styleClass += "logo-label" }
)
logoLabel.spacing = 10.0
logoLabel.alignment = Pos.CENTER_LEFT
HBox.setHgrow(logoLabel, Priority.ALWAYS)
logoLabel.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
dateLabel = Label("").apply { styleClass += "date-label" }
// Buttons area. In presentation mode there are no controls visible and you must use the keyboard.
val hbox = if (presentationMode) {
HBox(logoLabel, dateLabel).apply { styleClass += "controls-hbox" }
} else {
HBox(logoLabel, dateLabel, simulateInitialisationCheckbox, runPauseButton, nextButton, resetButton, styleChoice).apply { styleClass += "controls-hbox" }
}
hbox.styleClass += "fat-buttons"
hbox.spacing = 20.0
hbox.alignment = Pos.CENTER_RIGHT
hbox.padding = Insets(10.0, 20.0, 10.0, 20.0)
val vbox = VBox(hbox, dropShadow)
vbox.styleClass += "controls-vbox"
vbox.setPrefSize(Region.USE_COMPUTED_SIZE, Region.USE_COMPUTED_SIZE)
vbox.setMaxSize(Region.USE_COMPUTED_SIZE, Region.USE_PREF_SIZE)
return vbox
}
// TODO: Extract this to a real widget.
fun buildProgressTrackerWidget(label: String, tracker: ProgressTracker): TrackerWidget {
val allSteps: List<Pair<Int, ProgressTracker.Step>> = tracker.allSteps
val stepsBox = VBox().apply {
styleClass += "progress-tracker-widget-steps"
}
for ((indent, step) in allSteps) {
val stepLabel = Label(step.label).apply { padding = Insets(0.0, 0.0, 0.0, indent * 15.0) }
stepsBox.children += StackPane(stepLabel)
}
val trackerCurrentStep = tracker.currentStepRecursive
val curStep = allSteps.indexOfFirst { it.second == trackerCurrentStep }
val arrowSize = 7.0
val cursor = Polygon(-arrowSize, -arrowSize, arrowSize, 0.0, -arrowSize, arrowSize).apply {
styleClass += "progress-tracker-cursor"
translateY = (Math.max(0, curStep - 1) * sideBarStepHeight) + (sideBarStepHeight / 2.0)
}
val cursorBox = Pane(cursor).apply {
styleClass += "progress-tracker-cursor-box"
minWidth = 25.0
}
val vbox: VBox?
HBox.setHgrow(stepsBox, Priority.ALWAYS)
val content = HBox(cursorBox, stepsBox)
// Make the title bar
val title = Label(label).apply { styleClass += "sidebar-title-label" }
StackPane.setAlignment(title, Pos.CENTER_LEFT)
vbox = VBox(StackPane(title), content)
vbox.padding = Insets(0.0, 0.0, 25.0, 0.0)
return TrackerWidget(vbox, cursorBox, title, cursor)
}
/**
* Update the current display style. MUST only be called on the UI
* thread.
*/
fun updateDisplayStyle(displayStyle: Style) {
requireNotNull(splitter)
splitter.items.remove(scrollPane!!)
scrollPane = buildScrollPane(backgroundColor, displayStyle)
splitter.items.add(scrollPane!!)
splitter.dividers[0].position = 0.3
mapImage.isVisible = when (displayStyle) {
Style.MAP -> true
Style.CIRCLE -> false
}
// TODO: Can any current bullets be re-routed in flight?
}
}

View File

@ -1,235 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap
import javafx.animation.*
import javafx.geometry.Pos
import javafx.scene.control.Label
import javafx.scene.layout.StackPane
import javafx.scene.shape.Circle
import javafx.scene.shape.Line
import javafx.util.Duration
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.ProgressTracker
import net.corda.finance.utils.ScreenCoordinate
import net.corda.netmap.simulation.IRSSimulation
import net.corda.netmap.simulation.place
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.internal.InternalMockNetwork
import java.util.*
class VisualiserViewModel {
enum class Style {
MAP, CIRCLE;
override fun toString(): String {
return name.toLowerCase().capitalize()
}
}
inner class NodeWidget(val node: InternalMockNetwork.MockNode, val innerDot: Circle, val outerDot: Circle, val longPulseDot: Circle,
val pulseAnim: Animation, val longPulseAnim: Animation,
val nameLabel: Label, val statusLabel: Label) {
fun position(nodeCoords: (node: InternalMockNetwork.MockNode) -> ScreenCoordinate) {
val (x, y) = nodeCoords(node)
innerDot.centerX = x
innerDot.centerY = y
outerDot.centerX = x
outerDot.centerY = y
longPulseDot.centerX = x
longPulseDot.centerY = y
(nameLabel.parent as StackPane).relocate(x - 270.0, y - 10.0)
(statusLabel.parent as StackPane).relocate(x + 20.0, y - 10.0)
}
}
internal lateinit var view: VisualiserView
var presentationMode: Boolean = false
var simulation = IRSSimulation(true, false, null) // Manually pumped.
val trackerBoxes = HashMap<ProgressTracker, TrackerWidget>()
val doneTrackers = ArrayList<ProgressTracker>()
val nodesToWidgets = HashMap<InternalMockNetwork.MockNode, NodeWidget>()
var bankCount: Int = 0
var serviceCount: Int = 0
var stepDuration: Duration = Duration.millis(500.0)
var runningPausedState: NetworkMapVisualiser.RunningPausedState = NetworkMapVisualiser.RunningPausedState.Paused()
var displayStyle: Style = Style.MAP
set(value) {
field = value
view.updateDisplayStyle(value)
repositionNodes()
view.bindHideButtonPosition()
}
fun repositionNodes() {
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank]!!.position(when (displayStyle) {
Style.MAP -> { node -> nodeMapCoords(node) }
Style.CIRCLE -> { _ -> nodeCircleCoords(NetworkMapVisualiser.NodeType.BANK, index) }
})
}
for ((index, serviceProvider) in (simulation.serviceProviders + simulation.regulators).withIndex()) {
nodesToWidgets[serviceProvider]!!.position(when (displayStyle) {
Style.MAP -> { node -> nodeMapCoords(node) }
Style.CIRCLE -> { _ -> nodeCircleCoords(NetworkMapVisualiser.NodeType.SERVICE, index) }
})
}
}
fun nodeMapCoords(node: InternalMockNetwork.MockNode): ScreenCoordinate {
// For an image of the whole world, we use:
// return node.place.coordinate.project(mapImage.fitWidth, mapImage.fitHeight, 85.0511, -85.0511, -180.0, 180.0)
// For Europe, our bounds are: (lng,lat)
// bottom left: -23.2031,29.8406
// top right: 33.0469,64.3209
try {
return node.place.coordinate.project(view.mapImage.fitWidth, view.mapImage.fitHeight, 64.3209, 29.8406, -23.2031, 33.0469)
} catch (e: Exception) {
throw Exception("Cannot project ${node.started!!.info.singleIdentity()}", e)
}
}
fun nodeCircleCoords(type: NetworkMapVisualiser.NodeType, index: Int): ScreenCoordinate {
val stepRad: Double = when (type) {
NetworkMapVisualiser.NodeType.BANK -> 2 * Math.PI / bankCount
NetworkMapVisualiser.NodeType.SERVICE -> (2 * Math.PI / serviceCount)
}
val tangentRad: Double = stepRad * index + when (type) {
NetworkMapVisualiser.NodeType.BANK -> 0.0
NetworkMapVisualiser.NodeType.SERVICE -> Math.PI / 2
}
val radius = when (type) {
NetworkMapVisualiser.NodeType.BANK -> Math.min(view.stageWidth, view.stageHeight) / 3.5
NetworkMapVisualiser.NodeType.SERVICE -> Math.min(view.stageWidth, view.stageHeight) / 8
}
val xOffset = -220
val yOffset = -80
val circleX = view.stageWidth / 2 + xOffset
val circleY = view.stageHeight / 2 + yOffset
val x: Double = radius * Math.cos(tangentRad) + circleX
val y: Double = radius * Math.sin(tangentRad) + circleY
return ScreenCoordinate(x, y)
}
fun createNodes() {
bankCount = simulation.banks.size
serviceCount = simulation.serviceProviders.size + simulation.regulators.size
for ((index, bank) in simulation.banks.withIndex()) {
nodesToWidgets[bank] = makeNodeWidget(bank, "bank", bank.configuration.myLegalName, NetworkMapVisualiser.NodeType.BANK, index)
}
for ((index, service) in simulation.serviceProviders.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "network-service", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index)
}
for ((index, service) in simulation.regulators.withIndex()) {
nodesToWidgets[service] = makeNodeWidget(service, "regulator", service.configuration.myLegalName, NetworkMapVisualiser.NodeType.SERVICE, index + simulation.serviceProviders.size)
}
}
fun makeNodeWidget(forNode: InternalMockNetwork.MockNode, type: String, label: CordaX500Name = CordaX500Name(organisation = "Bank of Bologna", locality = "Bologna", country = "IT"),
nodeType: NetworkMapVisualiser.NodeType, index: Int): NodeWidget {
fun emitRadarPulse(initialRadius: Double, targetRadius: Double, duration: Double): Pair<Circle, Animation> {
val pulse = Circle(initialRadius).apply {
styleClass += "node-$type"
styleClass += "node-circle-pulse"
}
val animation = Timeline(
KeyFrame(Duration.seconds(0.0),
pulse.radiusProperty().keyValue(initialRadius),
pulse.opacityProperty().keyValue(1.0)
),
KeyFrame(Duration.seconds(duration),
pulse.radiusProperty().keyValue(targetRadius),
pulse.opacityProperty().keyValue(0.0)
)
)
return Pair(pulse, animation)
}
val innerDot = Circle(10.0).apply {
styleClass += "node-$type"
styleClass += "node-circle-inner"
}
val (outerDot, pulseAnim) = emitRadarPulse(10.0, 50.0, 0.45)
val (longPulseOuterDot, longPulseAnim) = emitRadarPulse(10.0, 100.0, 1.45)
view.root.children += outerDot
view.root.children += longPulseOuterDot
view.root.children += innerDot
val nameLabel = Label(label.organisation)
val nameLabelRect = StackPane(nameLabel).apply {
styleClass += "node-label"
alignment = Pos.CENTER_RIGHT
// This magic min width depends on the longest label of all nodes we may have, which we aren't calculating.
// TODO: Dynamically adjust it depending on the longest label to display.
minWidth = 250.0
}
view.root.children += nameLabelRect
val statusLabel = Label("")
val statusLabelRect = StackPane(statusLabel).apply { styleClass += "node-status-label" }
view.root.children += statusLabelRect
val widget = NodeWidget(forNode, innerDot, outerDot, longPulseOuterDot, pulseAnim, longPulseAnim, nameLabel, statusLabel)
when (displayStyle) {
Style.CIRCLE -> widget.position { _ -> nodeCircleCoords(nodeType, index) }
Style.MAP -> widget.position { node -> nodeMapCoords(node) }
}
return widget
}
fun fireBulletBetweenNodes(senderNode: InternalMockNetwork.MockNode, destNode: InternalMockNetwork.MockNode, startType: String, endType: String) {
val sx = nodesToWidgets[senderNode]!!.innerDot.centerX
val sy = nodesToWidgets[senderNode]!!.innerDot.centerY
val dx = nodesToWidgets[destNode]!!.innerDot.centerX
val dy = nodesToWidgets[destNode]!!.innerDot.centerY
val bullet = Circle(3.0)
bullet.styleClass += "bullet"
bullet.styleClass += "connection-$startType-to-$endType"
with(TranslateTransition(stepDuration, bullet)) {
fromX = sx
fromY = sy
toX = dx
toY = dy
setOnFinished {
// For some reason removing/adding the bullet nodes causes an annoying 1px shift in the map view, so
// to avoid visual distraction we just deliberately leak the bullet node here. Obviously this is a
// memory leak that would break long term usage.
//
// TODO: Find root cause and fix.
//
// root.children.remove(bullet)
bullet.isVisible = false
}
play()
}
val line = Line(sx, sy, dx, dy).apply { styleClass += "message-line" }
// Fade in quick, then fade out slow.
with(FadeTransition(stepDuration.divide(5.0), line)) {
fromValue = 0.0
toValue = 1.0
play()
setOnFinished {
with(FadeTransition(stepDuration.multiply(6.0), line)) { fromValue = 1.0; toValue = 0.0; play() }
}
}
view.root.children.add(1, line)
view.root.children.add(bullet)
}
}

View File

@ -1,182 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap.simulation
import co.paralleluniverse.fibers.Suspendable
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import net.corda.client.jackson.JacksonSupport
import net.corda.core.contracts.StateAndRef
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.FlowSession
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.internal.uncheckedCast
import net.corda.core.node.services.queryBy
import net.corda.core.toFuture
import net.corda.core.transactions.SignedTransaction
import net.corda.finance.flows.TwoPartyDealFlow.Acceptor
import net.corda.finance.flows.TwoPartyDealFlow.AutoOffer
import net.corda.finance.flows.TwoPartyDealFlow.Instigator
import net.corda.finance.plugin.registerFinanceJSONMappers
import net.corda.irs.contract.InterestRateSwap
import net.corda.irs.flows.FixingFlow
import net.corda.testing.core.singleIdentity
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.internal.startFlow
import net.corda.testing.node.makeTestIdentityService
import rx.Observable
import java.time.LocalDate
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.allOf
/**
* A simulation in which banks execute interest rate swaps with each other, including the fixing events.
*/
class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(networkSendManuallyPumped, runAsync, latencyInjector) {
lateinit var om: ObjectMapper
init {
currentDateAndTime = LocalDate.of(2016, 3, 8).atStartOfDay()
}
private val executeOnNextIteration = Collections.synchronizedList(LinkedList<() -> Unit>())
override fun startMainSimulation(): CompletableFuture<Unit> {
om = JacksonSupport.createInMemoryMapper(makeTestIdentityService(*(banks + regulators + ratesOracle).flatMap { it.started!!.info.legalIdentitiesAndCerts }.toTypedArray()))
registerFinanceJSONMappers(om)
return startIRSDealBetween(0, 1).thenCompose {
val future = CompletableFuture<Unit>()
// Next iteration is a pause.
executeOnNextIteration.add {}
executeOnNextIteration.add {
// Keep fixing until there's no more left to do.
val initialFixFuture = doNextFixing(0, 1)
fun onFailure(t: Throwable) {
future.completeExceptionally(t)
}
fun onSuccess() {
// Pause for an iteration.
executeOnNextIteration.add {}
executeOnNextIteration.add {
val f = doNextFixing(0, 1)
if (f != null) {
f.handle { _, throwable ->
if (throwable == null) {
onSuccess()
} else {
onFailure(throwable)
}
}
} else {
// All done!
future.complete(Unit)
}
}
}
initialFixFuture!!.handle { _, throwable ->
if (throwable == null) {
onSuccess()
} else {
onFailure(throwable)
}
}
}
future
}
}
private fun doNextFixing(i: Int, j: Int): CompletableFuture<Void>? {
println("Doing a fixing between $i and $j")
val node1 = banks[i].started!!
val node2 = banks[j].started!!
val swaps = node1.services.vaultService.queryBy<InterestRateSwap.State>().states
val theDealRef: StateAndRef<InterestRateSwap.State> = swaps.single()
// Do we have any more days left in this deal's lifetime? If not, return.
val nextFixingDate = theDealRef.state.data.calculation.nextFixingDate() ?: return null
extraNodeLabels[node1.internals] = "Fixing event on $nextFixingDate"
extraNodeLabels[node2.internals] = "Fixing event on $nextFixingDate"
// Complete the future when the state has been consumed on both nodes
val futA = node1.services.vaultService.whenConsumed(theDealRef.ref)
val futB = node2.services.vaultService.whenConsumed(theDealRef.ref)
showConsensusFor(listOf(node1.internals, node2.internals, regulators[0]))
// For some reason the first fix is always before the effective date.
if (nextFixingDate > currentDateAndTime.toLocalDate())
currentDateAndTime = nextFixingDate.atTime(15, 0)
return allOf(futA.toCompletableFuture(), futB.toCompletableFuture())
}
private fun startIRSDealBetween(i: Int, j: Int): CompletableFuture<SignedTransaction> {
val node1 = banks[i].started!!
val node2 = banks[j].started!!
extraNodeLabels[node1.internals] = "Setting up deal"
extraNodeLabels[node2.internals] = "Setting up deal"
// We load the IRS afresh each time because the leg parts of the structure aren't data classes so they don't
// have the convenient copy() method that'd let us make small adjustments. Instead they're partly mutable.
// TODO: We should revisit this in post-Excalibur cleanup and fix, e.g. by introducing an interface.
val resourceAsStream = javaClass.classLoader.getResourceAsStream("net/corda/irs/web/simulation/trade.json")
?: error("Trade representation cannot be loaded.")
val irs = om.readValue<InterestRateSwap.State>(resourceAsStream
.reader()
.readText()
.replace("oracleXXX", RatesOracleNode.RATES_SERVICE_NAME.toString()))
irs.fixedLeg.fixedRatePayer = node1.info.singleIdentity()
irs.floatingLeg.floatingRatePayer = node2.info.singleIdentity()
node1.registerInitiatedFlow(FixingFlow.Fixer::class.java)
node2.registerInitiatedFlow(FixingFlow.Fixer::class.java)
@InitiatingFlow
class StartDealFlow(val otherParty: Party,
val payload: AutoOffer) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val session = initiateFlow(otherParty)
return subFlow(Instigator(session, payload))
}
}
@InitiatedBy(StartDealFlow::class)
class AcceptDealFlow(otherSession: FlowSession) : Acceptor(otherSession)
val acceptDealFlows: Observable<AcceptDealFlow> = node2.registerInitiatedFlow(AcceptDealFlow::class.java)
val acceptorTxFuture = acceptDealFlows.toFuture().toCompletableFuture().thenCompose {
uncheckedCast<FlowStateMachine<*>, FlowStateMachine<SignedTransaction>>(it.stateMachine).resultFuture.toCompletableFuture()
}
showProgressFor(listOf(node1, node2))
showConsensusFor(listOf(node1.internals, node2.internals, regulators[0]))
val instigator = StartDealFlow(
node2.info.singleIdentity(),
AutoOffer(mockNet.defaultNotaryIdentity, irs)) // TODO Pass notary as parameter to Simulation.
val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture
return allOf(instigatorTxFuture.toCompletableFuture(), acceptorTxFuture).thenCompose { instigatorTxFuture.toCompletableFuture() }
}
override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
if (executeOnNextIteration.isNotEmpty())
executeOnNextIteration.removeAt(0)()
return super.iterate()
}
}

View File

@ -1,215 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap.simulation
import com.nhaarman.mockito_kotlin.doReturn
import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.CordaX500Name
import net.corda.core.utilities.ProgressTracker
import net.corda.finance.utils.CityDatabase
import net.corda.irs.api.NodeInterestRates
import net.corda.node.internal.StartedNode
import net.corda.node.services.statemachine.StateMachineManager
import net.corda.testing.core.TestIdentity
import net.corda.testing.node.InMemoryMessagingNetwork
import net.corda.testing.node.MockServices.Companion.makeTestDataSourceProperties
import net.corda.testing.node.TestClock
import net.corda.testing.node.internal.InternalMockNetwork
import net.corda.testing.node.internal.InternalMockNodeParameters
import net.corda.testing.node.internal.MockNodeArgs
import rx.Observable
import rx.subjects.PublishSubject
import java.math.BigInteger
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.util.*
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CompletableFuture.allOf
import java.util.concurrent.Future
internal val InternalMockNetwork.MockNode.place get() = configuration.myLegalName.locality.let { CityDatabase[it] }!!
/**
* Base class for network simulations that are based on the unit test / mock environment.
*
* Sets up some nodes that can run flows between each other, and exposes their progress trackers. Provides banks
* in a few cities around the world.
*/
abstract class Simulation(val networkSendManuallyPumped: Boolean,
runAsync: Boolean,
latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) {
private companion object {
val defaultParams // The get() is necessary so that entropyRoot isn't shared.
get() = InternalMockNodeParameters(configOverrides = {
doReturn(makeTestDataSourceProperties(it.myLegalName.organisation)).whenever(it).dataSourceProperties
})
val DUMMY_REGULATOR = TestIdentity(CordaX500Name("Regulator A", "Paris", "FR"), 100).party
}
init {
if (!runAsync && latencyInjector != null)
throw IllegalArgumentException("The latency injector is only useful when using manual pumping.")
}
val bankLocations = listOf(Pair("London", "GB"), Pair("Frankfurt", "DE"), Pair("Rome", "IT"))
class RatesOracleNode(args: MockNodeArgs) : InternalMockNetwork.MockNode(args) {
companion object {
// TODO: Make a more realistic legal name
val RATES_SERVICE_NAME = CordaX500Name(organisation = "Rates Service Provider", locality = "Madrid", country = "ES")
}
override fun start() = super.start().apply {
registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java)
registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java)
javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use {
services.cordaService(NodeInterestRates.Oracle::class.java).uploadFixes(it.reader().readText())
}
}
}
val mockNet = InternalMockNetwork(
cordappPackages = listOf("net.corda.finance.contract", "net.corda.irs"),
networkSendManuallyPumped = networkSendManuallyPumped,
threadPerNode = runAsync)
// TODO: Regulatory nodes don't actually exist properly, this is a last minute demo request.
// So we just fire a message at a node that doesn't know how to handle it, and it'll ignore it.
// But that's fine for visualisation purposes.
val regulators = listOf(mockNet.createUnstartedNode(defaultParams.copy(legalName = DUMMY_REGULATOR.name)))
val ratesOracle = mockNet.createUnstartedNode(defaultParams.copy(legalName = RatesOracleNode.RATES_SERVICE_NAME), ::RatesOracleNode)
// All nodes must be in one of these two lists for the purposes of the visualiser tool.
val serviceProviders: List<InternalMockNetwork.MockNode> = listOf(mockNet.defaultNotaryNode.internals, ratesOracle)
val banks: List<InternalMockNetwork.MockNode> = bankLocations.mapIndexed { i, (city, country) ->
val legalName = CordaX500Name(organisation = "Bank ${'A' + i}", locality = city, country = country)
// Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable.
mockNet.createUnstartedNode(defaultParams.copy(legalName = legalName, entropyRoot = BigInteger.valueOf(i.toLong())))
}
val clocks = (serviceProviders + regulators + banks).map { it.platformClock as TestClock }
// These are used from the network visualiser tool.
private val _allFlowSteps = PublishSubject.create<Pair<InternalMockNetwork.MockNode, ProgressTracker.Change>>()
private val _doneSteps = PublishSubject.create<Collection<InternalMockNetwork.MockNode>>()
@Suppress("unused")
val allFlowSteps: Observable<Pair<InternalMockNetwork.MockNode, ProgressTracker.Change>> = _allFlowSteps
@Suppress("unused")
val doneSteps: Observable<Collection<InternalMockNetwork.MockNode>> = _doneSteps
private var pumpCursor = 0
/**
* The current simulated date. By default this never changes. If you want it to change, you should do so from
* within your overridden [iterate] call. Changes in the current day surface in the [dateChanges] observable.
*/
var currentDateAndTime: LocalDateTime = LocalDate.now().atStartOfDay()
protected set(value) {
field = value
_dateChanges.onNext(value)
}
private val _dateChanges = PublishSubject.create<LocalDateTime>()
val dateChanges: Observable<LocalDateTime> get() = _dateChanges
init {
// Advance node clocks when current time is changed
dateChanges.subscribe {
val instant = currentDateAndTime.toInstant(ZoneOffset.UTC)
clocks.forEach { it.setTo(instant) }
}
}
/**
* A place for simulations to stash human meaningful text about what the node is "thinking", which might appear
* in the UI somewhere.
*/
val extraNodeLabels: MutableMap<InternalMockNetwork.MockNode, String> = Collections.synchronizedMap(HashMap())
/**
* Iterates the simulation by one step.
*
* The default implementation circles around the nodes, pumping until one of them handles a message. The next call
* will carry on from where this one stopped. In an environment where you want to take actions between anything
* interesting happening, or control the precise speed at which things operate (beyond the latency injector), this
* is a useful way to do things.
*
* @return the message that was processed, or null if no node accepted a message in this round.
*/
open fun iterate(): InMemoryMessagingNetwork.MessageTransfer? {
if (networkSendManuallyPumped) {
mockNet.messagingNetwork.pumpSend(false)
}
// Keep going until one of the nodes has something to do, or we have checked every node.
val endpoints = mockNet.messagingNetwork.endpointsExternal
var countDown = endpoints.size
while (countDown > 0) {
val handledMessage = endpoints[pumpCursor].pumpReceive(false)
if (handledMessage != null)
return handledMessage
// If this node had nothing to do, advance the cursor with wraparound and try again.
pumpCursor = (pumpCursor + 1) % endpoints.size
countDown--
}
return null
}
protected fun showProgressFor(nodes: List<StartedNode<InternalMockNetwork.MockNode>>) {
nodes.forEach { node ->
node.smm.changes.filter { it is StateMachineManager.Change.Add }.subscribe {
linkFlowProgress(node.internals, it.logic)
}
}
}
private fun linkFlowProgress(node: InternalMockNetwork.MockNode, flow: FlowLogic<*>) {
val pt = flow.progressTracker ?: return
pt.changes.subscribe { change: ProgressTracker.Change ->
// Runs on node thread.
_allFlowSteps.onNext(Pair(node, change))
}
}
protected fun showConsensusFor(nodes: List<InternalMockNetwork.MockNode>) {
val node = nodes.first()
node.started!!.smm.changes.filter { it is StateMachineManager.Change.Add }.first().subscribe {
linkConsensus(nodes, it.logic)
}
}
private fun linkConsensus(nodes: Collection<InternalMockNetwork.MockNode>, flow: FlowLogic<*>) {
flow.progressTracker?.changes?.subscribe { _: ProgressTracker.Change ->
// Runs on node thread.
if (flow.progressTracker!!.currentStep == ProgressTracker.DONE) {
_doneSteps.onNext(nodes)
}
}
}
val networkInitialisationFinished: CompletableFuture<Void> = allOf(*mockNet.nodes.map { it.nodeReadyFuture.toCompletableFuture() }.toTypedArray())
fun start(): Future<Unit> {
mockNet.startNodes()
// Wait for all the nodes to have finished registering with the network map service.
return networkInitialisationFinished.thenCompose { startMainSimulation() }
}
/**
* Sub-classes should override this to trigger whatever they want to simulate. This method will be invoked once the
* network bringup has been simulated.
*/
protected abstract fun startMainSimulation(): CompletableFuture<Unit>
fun stop() {
mockNet.stopNodes()
}
}

View File

@ -1,89 +0,0 @@
{
"fixedLeg": {
"fixedRatePayer": "GfHq2tTVk9z4eXgyUEefbHpUFfpnDvsFoZVZe3ikrLbwdRA4jebSJPykJwgw",
"notional": "$25000000",
"paymentFrequency": "SemiAnnual",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
"terminationDate": "2026-03-11",
"terminationDateAdjustment": null,
"fixedRate": {
"ratioUnit": {
"value": "0.01676"
}
},
"dayCountBasisDay": "D30",
"dayCountBasisYear": "Y360",
"rollConvention": "ModifiedFollowing",
"dayInMonth": 10,
"paymentRule": "InArrears",
"paymentDelay": 0,
"paymentCalendar": "London",
"interestPeriodAdjustment": "Adjusted"
},
"floatingLeg": {
"floatingRatePayer": "GfHq2tTVk9z4eXgyMYwWRYKSgGpARSquPTt8V4Z54RmNe2SJ7BUq2jSUUfvT",
"notional": {
"quantity": 2500000000,
"token": "USD"
},
"paymentFrequency": "Quarterly",
"effectiveDate": "2016-03-11",
"effectiveDateAdjustment": null,
"terminationDate": "2026-03-11",
"terminationDateAdjustment": null,
"dayCountBasisDay": "D30",
"dayCountBasisYear": "Y360",
"rollConvention": "ModifiedFollowing",
"fixingRollConvention": "ModifiedFollowing",
"dayInMonth": 10,
"resetDayInMonth": 10,
"paymentRule": "InArrears",
"paymentDelay": 0,
"paymentCalendar": [ "London" ],
"interestPeriodAdjustment": "Adjusted",
"fixingPeriodOffset": 2,
"resetRule": "InAdvance",
"fixingsPerPayment": "Quarterly",
"fixingCalendar": [ "NewYork" ],
"index": "ICE LIBOR",
"indexSource": "Rates Service Provider",
"indexTenor": {
"name": "3M"
}
},
"calculation": {
"expression": "( fixedLeg.notional.quantity * (fixedLeg.fixedRate.ratioUnit.value)) -(floatingLeg.notional.quantity * (calculation.fixingSchedule.get(context.getDate('currentDate')).rate.ratioUnit.value))",
"floatingLegPaymentSchedule": {
},
"fixedLegPaymentSchedule": {
}
},
"common": {
"baseCurrency": "EUR",
"eligibleCurrency": "EUR",
"eligibleCreditSupport": "Cash in an Eligible Currency",
"independentAmounts": "€0",
"threshold": "€0",
"minimumTransferAmount": "250000 EUR",
"rounding": "10000 EUR",
"valuationDateDescription": "Every Local Business Day",
"notificationTime": "2:00pm London",
"resolutionTime": "2:00pm London time on the first LocalBusiness Day following the date on which the notice is given ",
"interestRate": {
"oracle": "C=ES,L=Madrid,O=Rates Service Provider",
"tenor": {
"name": "6M"
},
"ratioUnit": null,
"name": "EONIA"
},
"addressForTransfers": "",
"exposure": {},
"localBusinessDay": [ "London" , "NewYork" ],
"dailyInterestAmount": "(CashAmount * InterestRate ) / (fixedLeg.notional.token.currencyCode.equals('GBP')) ? 365 : 360",
"tradeID": "tradeXXX",
"hashLegalDocs": "put hash here"
},
"oracle": "oracleXXX"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

View File

@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<!--
~ R3 Proprietary and Confidential
~
~ Copyright (c) 2018 R3 Limited. All rights reserved.
~
~ The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
~
~ Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
-->
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/8.0.65" xmlns:fx="http://javafx.com/fxml/1">
<children>
<HBox alignment="CENTER_RIGHT" prefHeight="0.0" prefWidth="600.0">
<children>
<Button fx:id="nextButton" mnemonicParsing="false" onAction="#onNextClicked" text="Next"/>
</children>
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0"/>
</padding>
</HBox>
<SplitPane dividerPositions="0.2408026755852843" prefHeight="336.0" prefWidth="600.0" VBox.vgrow="ALWAYS">
<items>
<AnchorPane minHeight="0.0" minWidth="0.0" prefHeight="160.0" prefWidth="100.0"/>
<ScrollPane fx:id="mapView" hbarPolicy="NEVER" pannable="true" prefHeight="200.0" prefWidth="200.0"
vbarPolicy="NEVER"/>
</items>
</SplitPane>
</children>
</VBox>

View File

@ -1,43 +0,0 @@
Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the copyright statement(s).
"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 469 KiB

View File

@ -1,228 +0,0 @@
.root-pane {
-fx-font-family: "Source Sans Pro", sans-serif;
-fx-font-size: 12pt;
}
.node-bank {
-base-fill: #9e7bff;
}
.node-regulator {
-base-fill: #fff2d1;
}
.node-network-service {
-base-fill: red;
}
.node-circle-inner {
-fx-fill: -base-fill;
-fx-stroke: derive(-base-fill, -40%);
-fx-stroke-width: 2px;
}
.node-circle-pulse {
-fx-fill: radial-gradient(center 50% 50%, radius 50%, #ffffff00, derive(-base-fill, 50%));
}
.hide-sidebar-button {
-fx-background-color: linear-gradient(to left, #464646, derive(#1c1c1c, 10%));
-fx-min-width: 0;
-fx-text-fill: #ffffffaa;
-fx-alignment: top-right;
-fx-label-padding: 0;
-fx-padding: 0 10 0 10;
-fx-border-color: #00000066;
-fx-border-width: 1 0 1 1;
}
.bullet {
-fx-fill: black;
}
.connection-bank-to-bank {
-fx-fill: white;
}
.message-line {
-fx-stroke: white;
}
.connection-bank-to-regulator {
-fx-stroke: red;
}
.node-label > Label, .node-status-label > Label {
-fx-text-fill: white;
-fx-effect: dropshadow(gaussian, black, 10, 0.1, 0, 0);
}
/* Hack around the Modena theme that makes all scroll panes grey by default */
.scroll-pane > .viewport {
-fx-background-color: transparent;
}
.scroll-pane .scroll-bar {
-fx-background-color: transparent;
}
.flat-button {
-fx-background-color: white;
-fx-padding: 0 0 0 0;
}
.flat-button:hover {
-fx-underline: true;
-fx-cursor: hand;
}
.flat-button:focused {
-fx-font-weight: bold;
}
.fat-buttons Button {
-fx-padding: 10 15 10 15;
-fx-min-width: 100;
-fx-font-weight: bold;
-fx-base: whitesmoke;
}
.fat-buttons ChoiceBox {
-fx-padding: 4 8 4 8;
-fx-min-width: 100;
-fx-font-weight: bold;
-fx-base: whitesmoke;
}
.fat-buttons Button:default {
-fx-base: orange;
-fx-text-fill: white;
-fx-font-family: 'Source Sans Pro', sans-serif;
}
.fat-buttons Button:cancel {
-fx-background-color: white;
-fx-background-insets: 1;
-fx-border-color: lightgray;
-fx-border-radius: 3;
-fx-text-fill: black;
}
.fat-buttons Button:cancel:hover {
-fx-base: white;
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-text-fill: black;
}
/** take out the focus ring */
.no-focus-button:focused {
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
-fx-background-radius: 3px, 3px, 2px, 1px;
}
.blue-button {
-fx-base: lightblue;
-fx-text-fill: darkslategrey;
}
.blue-button:disabled {
-fx-text-fill: white;
}
.green-button {
-fx-base: #62c462;
-fx-text-fill: darkslategrey;
}
.green-button:disabled {
-fx-text-fill: white;
}
.next-button {
-fx-base: #66b2ff;
-fx-text-fill: white;
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
-fx-background-radius: 3px, 3px, 2px, 1px;
}
.style-choice:default {
-fx-base: #66b2ff;
-fx-text-fill: white;
-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
-fx-background-insets: 0 0 -1 0, 0, 1, 2;
-fx-background-radius: 3px, 3px, 2px, 1px;
}
.controls-hbox {
-fx-background-color: white;
}
.drop-shadow-pane-horizontal {
/*-fx-background-color: linear-gradient(to top, #888, #fff);*/
-fx-background-color: white;
-fx-border-color: #555;
-fx-border-width: 0 0 1 0;
}
.logo-label {
-fx-font-size: 40;
}
.date-label {
-fx-font-size: 30;
}
.splitter {
-fx-padding: 0;
-fx-background-color: #464646;
}
.splitter > .split-pane-divider {
-fx-background-color: linear-gradient(to left, #1c1c1c, transparent);
-fx-border-color: black;
-fx-border-width: 0 1 0 0;
-fx-padding: 0 2 0 2;
}
.progress-tracker-cursor-box {
-fx-padding: 0 15 0 0;
}
.progress-tracker-cursor {
-fx-translate-x: 15.0;
-fx-fill: white;
}
.sidebar {
-fx-background-color: #464646;
}
.sidebar > VBox > StackPane {
-fx-background-color: #666666;
-fx-padding: 5px;
}
.sidebar > VBox > StackPane > Label {
-fx-text-fill: white;
}
.progress-tracker-widget-steps {
-fx-spacing: 5;
-fx-fill-width: true;
}
.progress-tracker-widget-steps > StackPane {
-fx-background-color: #5a5a5a;
-fx-padding: 7px;
-fx-alignment: center-left;
-fx-max-height: 35px;
-fx-min-height: 35px;
}
.progress-tracker-widget-steps > StackPane > Label {
-fx-text-fill: white;
}

View File

@ -1,29 +0,0 @@
/*
* R3 Proprietary and Confidential
*
* Copyright (c) 2018 R3 Limited. All rights reserved.
*
* The intellectual and technical concepts contained herein are proprietary to R3 and its suppliers and are protected by trade secret law.
*
* Distribution of this file or any portion thereof via any medium without the express permission of R3 is strictly prohibited.
*/
package net.corda.netmap.simulation
import net.corda.core.utilities.getOrThrow
import net.corda.testing.internal.LogHelper
import org.junit.Test
class IRSSimulationTest {
// TODO: These tests should be a lot more complete.
@Test
fun `runs to completion`() {
LogHelper.setLevel("+messages") // FIXME: Don't manipulate static state in tests.
val sim = IRSSimulation(false, false, null)
val future = sim.start()
while (!future.isDone) {
sim.iterate()
}
future.getOrThrow()
}
}

View File

@ -71,7 +71,6 @@ include 'samples:trader-demo'
include 'samples:irs-demo'
include 'samples:irs-demo:cordapp'
include 'samples:irs-demo:web'
include 'samples:network-visualiser'
include 'samples:simm-valuation-demo'
include 'samples:simm-valuation-demo:flows'
include 'samples:simm-valuation-demo:contracts-states'

View File

@ -51,6 +51,10 @@ artifacts {
}
}
jar {
classifier "ignore"
}
publish {
disableDefaultJar = true
name 'corda-tools-network-bootstrapper'