diff --git a/.gitignore b/.gitignore index f3ae176bcc..68b8472e9b 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ lib/dokka.jar .idea/libraries .idea/shelf .idea/dataSources +/gradle-plugins/.idea # Include the -parameters compiler option by default in IntelliJ required for serialization. !.idea/compiler.xml @@ -53,6 +54,7 @@ lib/dokka.jar # Gradle: # .idea/gradle.xml # .idea/libraries +/gradle-plugins/gradle* # Mongo Explorer plugin: # .idea/mongoSettings.xml @@ -65,6 +67,7 @@ lib/dokka.jar # IntelliJ /out/ +/classes/ # mpeltonen/sbt-idea plugin .idea_modules/ diff --git a/.idea/compiler.xml b/.idea/compiler.xml index d639c80f07..acd527bc35 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -17,6 +17,8 @@ + + @@ -59,10 +61,13 @@ + + + diff --git a/build.gradle b/build.gradle index bf86531ff6..54bc126c8b 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,11 @@ buildscript { // TODO: Sort this alphabetically. ext.kotlin_version = constants.getProperty("kotlinVersion") ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved. + + // gradle-capsule-plugin:1.0.2 contains capsule:1.0.1 + // TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3 + ext.capsule_version = '1.0.1' + ext.asm_version = '0.5.3' ext.artemis_version = '1.5.3' ext.jackson_version = '2.8.5' @@ -41,6 +46,7 @@ buildscript { ext.rxjava_version = '1.2.4' ext.requery_version = '1.2.1' ext.dokka_version = '0.9.13' + ext.eddsa_version = '0.2.0' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: ext.java8_minUpdateVersion = '131' @@ -60,12 +66,14 @@ buildscript { classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" classpath "org.ajoberstar:grgit:1.1.0" + classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment. } } plugins { // TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style, // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. + // Version 1.0.2 of this plugin uses capsule:1.0.1 id "us.kirchmeier.capsule" version "1.0.2" } @@ -249,7 +257,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver'] + publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt index 361313dbe3..100fc2f2f7 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule import net.corda.core.contracts.Amount import net.corda.core.contracts.BusinessCalendar import net.corda.core.crypto.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps @@ -43,18 +44,21 @@ object JacksonSupport { } class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName) override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) } class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName) override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) } class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + @Suppress("OverridingDeprecatedMember", "DEPRECATION") override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException() override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException() override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() @@ -66,6 +70,7 @@ object JacksonSupport { addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer) addSerializer(Party::class.java, PartySerializer) addDeserializer(Party::class.java, PartyDeserializer) + addDeserializer(AbstractParty::class.java, PartyDeserializer) addSerializer(BigDecimal::class.java, ToStringSerializer) addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer()) addSerializer(SecureHash::class.java, SecureHashSerializer) @@ -160,8 +165,20 @@ object JacksonSupport { } val mapper = parser.codec as PartyObjectMapper - val principal = X500Name(parser.text) - return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") + // TODO: We should probably have a better specified way of identifying X.500 names vs keys + // Base58 keys never include an equals character, while X.500 names always will, so we use that to determine + // how to parse the content + return if (parser.text.contains("=")) { + val principal = X500Name(parser.text) + mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") + } else { + val key = try { + parsePublicKeyBase58(parser.text) + } catch (e: Exception) { + throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key") + } + mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") + } } } diff --git a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt index 225bb9ec59..45454ef1d0 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/StringToMethodCallParser.kt @@ -193,8 +193,7 @@ open class StringToMethodCallParser @JvmOverloads constructor( val parameterString = "{ $args }" val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args) if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args) - val inOrderParams: List = parameters.mapIndexed { _, param -> - val (argName, argType) = param + val inOrderParams: List = parameters.mapIndexed { _, (argName, argType) -> val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) try { om.readValue(entry.traverse(om), argType) diff --git a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt index 7c46e4919b..7610fbf91b 100644 --- a/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt +++ b/client/jackson/src/test/kotlin/net/corda/jackson/StringToMethodCallParserTest.kt @@ -1,14 +1,16 @@ package net.corda.jackson import net.corda.core.crypto.SecureHash +import org.junit.Assert.assertArrayEquals import org.junit.Test +import kotlin.reflect.full.primaryConstructor import kotlin.test.assertEquals class StringToMethodCallParserTest { @Suppress("UNUSED") class Target { fun simple() = "simple" - fun string(note: String) = note + fun string(noteTextWord: String) = noteTextWord fun twoStrings(a: String, b: String) = a + b fun simpleObject(hash: SecureHash.SHA256) = hash.toString() fun complexObject(pair: Pair) = pair @@ -20,7 +22,7 @@ class StringToMethodCallParserTest { val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401" val tests = mapOf( "simple" to "simple", - "string note: A test of barewords" to "A test of barewords", + "string noteTextWord: A test of barewords" to "A test of barewords", "twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak", "simpleObject hash: $randomHash" to randomHash.toUpperCase(), "complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother"), @@ -36,4 +38,31 @@ class StringToMethodCallParserTest { assertEquals(output, parser.parse(target, input).invoke()) } } + + @Suppress("UNUSED") + class ConstructorTarget(val someWord: String, val aDifferentThing: Int) { + constructor(alternativeWord: String) : this(alternativeWord, 0) + } + + @Test + fun ctor1() { + val clazz = ConstructorTarget::class.java + val parser = StringToMethodCallParser(clazz) + val ctor = clazz.constructors.single { it.parameterCount == 2 } + val names: List = parser.paramNamesFromConstructor(ctor) + assertEquals(listOf("someWord", "aDifferentThing"), names) + val args: Array = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "someWord: Blah blah blah, aDifferentThing: 12") + assertArrayEquals(args, arrayOf("Blah blah blah", 12)) + } + + @Test + fun ctor2() { + val clazz = ConstructorTarget::class.java + val parser = StringToMethodCallParser(clazz) + val ctor = clazz.constructors.single { it.parameterCount == 1 } + val names: List = parser.paramNamesFromConstructor(ctor) + assertEquals(listOf("alternativeWord"), names) + val args: Array = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!") + assertArrayEquals(args, arrayOf("Foo bar!")) + } } \ No newline at end of file diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index ec52494023..95f0ed0fcc 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -11,6 +11,9 @@ configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime + + smokeTestCompile.extendsFrom compile + smokeTestRuntime.extendsFrom runtime } sourceSets { @@ -21,6 +24,24 @@ sourceSets { srcDir file('src/integration-test/kotlin') } } + smokeTest { + kotlin { + // We must NOT have any Node code on the classpath, so do NOT + // include the test or integrationTest dependencies here. + compileClasspath += main.output + runtimeClasspath += main.output + srcDir file('src/smoke-test/kotlin') + } + } +} + +processSmokeTestResources { + from(file("$rootDir/config/test/log4j2.xml")) { + rename 'log4j2\\.xml', 'log4j2-test.xml' + } + from(project(':node:capsule').tasks.buildCordaJAR) { + rename 'corda-(.*)', 'corda.jar' + } } // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in @@ -38,11 +59,22 @@ dependencies { testCompile project(':test-utils') testCompile project(':client:mock') - // Integration test helpers - integrationTestCompile "junit:junit:$junit_version" + // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':finance') + smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" + smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" + smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" + smokeTestCompile "org.assertj:assertj-core:${assertj_version}" + smokeTestCompile "junit:junit:$junit_version" } task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath } + +task smokeTest(type: Test) { + testClassesDir = sourceSets.smokeTest.output.classesDir + classpath = sourceSets.smokeTest.runtimeClasspath + systemProperties['build.dir'] = buildDir +} diff --git a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt index 5287a0de4f..3ecb3eda6e 100644 --- a/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt +++ b/client/rpc/src/integration-test/kotlin/net/corda/client/rpc/RPCStabilityTests.kt @@ -5,26 +5,181 @@ import com.esotericsoftware.kryo.Serializer import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.pool.KryoPool +import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures +import net.corda.client.rpc.internal.RPCClient +import net.corda.client.rpc.internal.RPCClientConfiguration +import net.corda.core.* import net.corda.core.messaging.RPCOps -import net.corda.core.millis -import net.corda.core.random63BitValue +import net.corda.node.driver.poll import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCKryo import net.corda.testing.* import org.apache.activemq.artemis.api.core.SimpleString +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue import org.junit.Test import rx.Observable import rx.subjects.PublishSubject import rx.subjects.UnicastSubject import java.time.Duration +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger class RPCStabilityTests { + object DummyOps : RPCOps { + override val protocolVersion = 0 + } + + private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Int { + val values = ConcurrentLinkedQueue() + return poll(executorService, "number of threads to become stable", 250.millis) { + values.add(Thread.activeCount()) + if (values.size > 5) { + values.poll() + } + val first = values.peek() + if (values.size == 5 && values.all { it == first }) { + first + } else { + null + } + }.get() + } + + @Test + fun `client and server dont leak threads`() { + val executor = Executors.newScheduledThreadPool(1) + fun startAndStop() { + rpcDriver { + val server = startRpcServer(ops = DummyOps) + startRpcClient(server.get().broker.hostAndPort!!).get() + } + } + repeat(5) { + startAndStop() + } + val numberOfThreadsBefore = waitUntilNumberOfThreadsStable(executor) + repeat(5) { + startAndStop() + } + val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor) + // This is a less than check because threads from other tests may be shutting down while this test is running. + // This is therefore a "best effort" check. When this test is run on its own this should be a strict equality. + assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter) + executor.shutdownNow() + } + + @Test + fun `client doesnt leak threads when it fails to start`() { + val executor = Executors.newScheduledThreadPool(1) + fun startAndStop() { + rpcDriver { + ErrorOr.catch { startRpcClient(HostAndPort.fromString("localhost:9999")).get() } + val server = startRpcServer(ops = DummyOps) + ErrorOr.catch { startRpcClient( + server.get().broker.hostAndPort!!, + configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) + ).get() } + } + } + repeat(5) { + startAndStop() + } + val numberOfThreadsBefore = waitUntilNumberOfThreadsStable(executor) + repeat(5) { + startAndStop() + } + val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor) + assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter) + executor.shutdownNow() + } + + fun RpcBrokerHandle.getStats(): Map { + return serverControl.run { + mapOf( + "connections" to listConnectionIDs().toSet(), + "sessionCount" to listConnectionIDs().flatMap { listSessions(it).toList() }.size, + "consumerCount" to totalConsumerCount + ) + } + } + + @Test + fun `rpc server close doesnt leak broker resources`() { + rpcDriver { + fun startAndCloseServer(broker: RpcBrokerHandle) { + startRpcServerWithBrokerRunning( + configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), + ops = DummyOps, + brokerHandle = broker + ).rpcServer.close() + } + + val broker = startRpcBroker().get() + startAndCloseServer(broker) + val initial = broker.getStats() + repeat(100) { + startAndCloseServer(broker) + } + pollUntilTrue("broker resources to be released") { + initial == broker.getStats() + } + } + } + + @Test + fun `rpc client close doesnt leak broker resources`() { + rpcDriver { + val server = startRpcServer(configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), ops = DummyOps).get() + RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password).close() + val initial = server.broker.getStats() + repeat(100) { + val connection = RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password) + connection.close() + } + pollUntilTrue("broker resources to be released") { + initial == server.broker.getStats() + } + } + } + + @Test + fun `rpc server close is idempotent`() { + rpcDriver { + val server = startRpcServer(ops = DummyOps).get() + repeat(10) { + server.rpcServer.close() + } + } + } + + @Test + fun `rpc client close is idempotent`() { + rpcDriver { + val serverShutdown = shutdownManager.follower() + val server = startRpcServer(ops = DummyOps).get() + serverShutdown.unfollow() + // With the server up + val connection1 = RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password) + repeat(10) { + connection1.close() + } + val connection2 = RPCClient(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password) + serverShutdown.shutdown() + // With the server down + repeat(10) { + connection2.close() + } + } + } + interface LeakObservableOps: RPCOps { fun leakObservable(): Observable } @@ -42,7 +197,7 @@ class RPCStabilityTests { } } val server = startRpcServer(ops = leakObservableOpsImpl) - val proxy = startRpcClient(server.get().hostAndPort).get() + val proxy = startRpcClient(server.get().broker.hostAndPort!!).get() // Leak many observables val N = 200 (1..N).toList().parallelStream().forEach { @@ -57,6 +212,31 @@ class RPCStabilityTests { } } + interface ReconnectOps : RPCOps { + fun ping(): String + } + + @Test + fun `client reconnects to rebooted server`() { + rpcDriver { + val ops = object : ReconnectOps { + override val protocolVersion = 0 + override fun ping() = "pong" + } + val serverFollower = shutdownManager.follower() + val serverPort = startRpcServer(ops = ops).getOrThrow().broker.hostAndPort!! + serverFollower.unfollow() + val clientFollower = shutdownManager.follower() + val client = startRpcClient(serverPort).getOrThrow() + clientFollower.unfollow() + assertEquals("pong", client.ping()) + serverFollower.shutdown() + startRpcServer(ops = ops, customPort = serverPort).getOrThrow() + assertEquals("pong", client.ping()) + clientFollower.shutdown() // Driver would do this after the new server, causing hang. + } + } + interface TrackSubscriberOps : RPCOps { fun subscribe(): Observable } @@ -86,7 +266,7 @@ class RPCStabilityTests { val numberOfClients = 4 val clients = Futures.allAsList((1 .. numberOfClients).map { - startRandomRpcClient(server.hostAndPort) + startRandomRpcClient(server.broker.hostAndPort!!) }).get() // Poll until all clients connect @@ -131,7 +311,7 @@ class RPCStabilityTests { // Construct an RPC session manually so that we can hang in the message handler val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" - val session = startArtemisSession(server.hostAndPort) + val session = startArtemisSession(server.broker.hostAndPort!!) session.createTemporaryQueue(myQueue, myQueue) val consumer = session.createConsumer(myQueue, null, -1, -1, false) consumer.setMessageHandler { @@ -163,7 +343,7 @@ class RPCStabilityTests { fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { pollUntilTrue("number of RPC clients to become $expected") { - val clientAddresses = server.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } + val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } clientAddresses.size == expected }.get() } \ No newline at end of file diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt index a6c97c3e3a..199cdd6d67 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/CordaRPCClient.kt @@ -9,10 +9,12 @@ import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.config.SSLConfiguration import java.time.Duration +/** @see RPCClient.RPCConnection */ class CordaRPCConnection internal constructor( connection: RPCClient.RPCConnection ) : RPCClient.RPCConnection by connection +/** @see RPCClientConfiguration */ data class CordaRPCClientConfiguration( val connectionMaxRetryInterval: Duration ) { @@ -29,6 +31,7 @@ data class CordaRPCClientConfiguration( } } +/** @see RPCClient */ class CordaRPCClient( hostAndPort: HostAndPort, sslConfiguration: SSLConfiguration? = null, diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt index 60d50928bd..3e52dbd946 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClient.kt @@ -53,10 +53,12 @@ data class RPCClientConfiguration( val connectionRetryIntervalMultiplier: Double, /** Maximum retry interval */ val connectionMaxRetryInterval: Duration, + val maxReconnectAttempts: Int, /** Maximum file size */ val maxFileSize: Int ) { companion object { + val unlimitedReconnectAttempts = -1 @JvmStatic val default = RPCClientConfiguration( minimumServerProtocolVersion = 0, @@ -68,6 +70,7 @@ data class RPCClientConfiguration( connectionRetryInterval = 5.seconds, connectionRetryIntervalMultiplier = 1.5, connectionMaxRetryInterval = 3.minutes, + maxReconnectAttempts = unlimitedReconnectAttempts, /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ maxFileSize = 10485760 ) @@ -114,9 +117,9 @@ class RPCClient( * * The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object * graph returned then the server-side observable is transparently forwarded to the client side here. - * *You are expected to use it*. The server will begin buffering messages immediately that it will expect you to - * drain by subscribing to the returned observer. You can opt-out of this by simply calling the - * [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually + * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the + * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply + * calling the [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually * subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself * is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are * unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't @@ -139,30 +142,37 @@ class RPCClient( retryInterval = rpcConfiguration.connectionRetryInterval.toMillis() retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis() + reconnectAttempts = rpcConfiguration.maxReconnectAttempts minLargeMessageSize = rpcConfiguration.maxFileSize } val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass) - proxyHandler.start() + try { + proxyHandler.start() - @Suppress("UNCHECKED_CAST") - val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I + @Suppress("UNCHECKED_CAST") + val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I - val serverProtocolVersion = ops.protocolVersion - if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) { - throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" + - " than the server's supported protocol version ($serverProtocolVersion)") - } - proxyHandler.setServerProtocolVersion(serverProtocolVersion) - - log.debug("RPC connected, returning proxy") - object : RPCConnection { - override val proxy = ops - override val serverProtocolVersion = serverProtocolVersion - override fun close() { - proxyHandler.close() - serverLocator.close() + val serverProtocolVersion = ops.protocolVersion + if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) { + throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" + + " than the server's supported protocol version ($serverProtocolVersion)") } + proxyHandler.setServerProtocolVersion(serverProtocolVersion) + + log.debug("RPC connected, returning proxy") + object : RPCConnection { + override val proxy = ops + override val serverProtocolVersion = serverProtocolVersion + override fun close() { + proxyHandler.close() + serverLocator.close() + } + } + } catch (exception: Throwable) { + proxyHandler.close() + serverLocator.close() + throw exception } } } diff --git a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt index da95f01b4d..2fde6fcaaf 100644 --- a/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt +++ b/client/rpc/src/main/kotlin/net/corda/client/rpc/internal/RPCClientProxyHandler.kt @@ -25,16 +25,11 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator import rx.Notification import rx.Observable import rx.subjects.UnicastSubject -import sun.reflect.CallerSensitive import java.lang.reflect.InvocationHandler import java.lang.reflect.Method import java.util.* -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit +import java.util.concurrent.* import java.util.concurrent.atomic.AtomicInteger -import kotlin.collections.ArrayList import kotlin.reflect.jvm.javaMethod /** @@ -81,16 +76,13 @@ class RPCClientProxyHandler( val log = loggerFor() // Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context // to do that. However it may still be used for serialisation of RPC requests and related messages. - val kryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build() + val kryoPool: KryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build() // To check whether toString() is being invoked val toStringMethod: Method = Object::toString.javaMethod!! } // Used for reaping - private val reaperExecutor = Executors.newScheduledThreadPool( - 1, - ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build() - ) + private var reaperExecutor: ScheduledExecutorService? = null // A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering. private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build() @@ -109,7 +101,7 @@ class RPCClientProxyHandler( hardReferenceStore = Collections.synchronizedSet(mutableSetOf>()) ) // Holds a reference to the scheduled reaper. - private lateinit var reaperScheduledFuture: ScheduledFuture<*> + private var reaperScheduledFuture: ScheduledFuture<*>? = null // The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion] private var serverProtocolVersion: Int? = null @@ -145,7 +137,7 @@ class RPCClientProxyHandler( // TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a // single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is // integrated properly. - private lateinit var sessionAndConsumer: ArtemisConsumer + private var sessionAndConsumer: ArtemisConsumer? = null // Pool producers to reduce contention on the client side. private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) { // Note how we create new sessions *and* session factories per producer. @@ -162,7 +154,12 @@ class RPCClientProxyHandler( * Start the client. This creates the per-client queue, starts the consumer session and the reaper. */ fun start() { - reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate( + lifeCycle.requireState(State.UNSTARTED) + reaperExecutor = Executors.newScheduledThreadPool( + 1, + ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build() + ) + reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( this::reapObservables, rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(), @@ -187,7 +184,7 @@ class RPCClientProxyHandler( if (method == toStringMethod) { return "Client RPC proxy for $rpcOpsClass" } - if (sessionAndConsumer.session.isClosed) { + if (sessionAndConsumer!!.session.isClosed) { throw RPCException("RPC Proxy is closed") } val rpcId = RPCApi.RpcRequestId(random63BitValue()) @@ -211,6 +208,12 @@ class RPCClientProxyHandler( it.session.commit() } return replyFuture.getOrThrow() + } catch (e: RuntimeException) { + // Already an unchecked exception, so just rethrow it + throw e + } catch (e: Exception) { + // This must be a checked exception, so wrap it + throw RPCException(e.message ?: "", e) } finally { callSiteMap?.remove(rpcId.toLong) } @@ -268,24 +271,19 @@ class RPCClientProxyHandler( * Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors. */ fun close() { - sessionAndConsumer.consumer.close() - sessionAndConsumer.session.close() - sessionAndConsumer.sessionFactory.close() - reaperScheduledFuture.cancel(false) + sessionAndConsumer?.sessionFactory?.close() + reaperScheduledFuture?.cancel(false) observableContext.observableMap.invalidateAll() reapObservables() - reaperExecutor.shutdownNow() + reaperExecutor?.shutdownNow() sessionAndProducerPool.close().forEach { - it.producer.close() - it.session.close() it.sessionFactory.close() } // Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may // leak borrowed executors. val observationExecutors = observationExecutorPool.close() observationExecutors.forEach { it.shutdownNow() } - observationExecutors.forEach { it.awaitTermination(100, TimeUnit.MILLISECONDS) } - lifeCycle.transition(State.STARTED, State.FINISHED) + lifeCycle.justTransition(State.FINISHED) } /** diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt new file mode 100644 index 0000000000..75c4074be1 --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt @@ -0,0 +1,48 @@ +package net.corda.kotlin.rpc + +import com.typesafe.config.* +import net.corda.core.crypto.commonName +import net.corda.core.identity.Party +import net.corda.nodeapi.User + +class NodeConfig( + val party: Party, + val p2pPort: Int, + val rpcPort: Int, + val webPort: Int, + val extraServices: List, + val users: List, + var networkMap: NodeConfig? = null +) { + companion object { + val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) + } + + val commonName: String = party.name.commonName + + /* + * The configuration object depends upon the networkMap, + * which is mutable. + */ + fun toFileConfig(): Config = ConfigFactory.empty() + .withValue("myLegalName", valueFor(party.name.toString())) + .withValue("p2pAddress", addressValueFor(p2pPort)) + .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) + .withFallback(optional("networkMapService", networkMap, { c, n -> + c.withValue("address", addressValueFor(n.p2pPort)) + .withValue("legalName", valueFor(n.party.name.toString())) + })) + .withValue("webAddress", addressValueFor(webPort)) + .withValue("rpcAddress", addressValueFor(rpcPort)) + .withValue("rpcUsers", valueFor(users.map(User::toMap).toList())) + .withValue("useTestClock", valueFor(true)) + + fun toText(): String = toFileConfig().root().render(renderOptions) + + private fun valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any) + private fun addressValueFor(port: Int) = valueFor("localhost:$port") + private inline fun optional(path: String, obj: T?, body: (Config, T) -> Config): Config { + val config = ConfigFactory.empty() + return if (obj == null) config else body(config, obj).atPath(path) + } +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt new file mode 100644 index 0000000000..3d81f9ad83 --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt @@ -0,0 +1,107 @@ +package net.corda.kotlin.rpc + +import com.google.common.net.HostAndPort +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCConnection +import net.corda.core.utilities.loggerFor +import java.io.File +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit.SECONDS +import kotlin.test.* + +class NodeProcess( + val config: NodeConfig, + val nodeDir: Path, + private val node: Process, + private val client: CordaRPCClient +) : AutoCloseable { + private companion object { + val log = loggerFor() + val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") + val corda = File(this::class.java.getResource("/corda.jar").toURI()) + val buildDir: Path = Paths.get(System.getProperty("build.dir")) + val capsuleDir: Path = buildDir.resolve("capsule") + } + + fun connect(): CordaRPCConnection { + val user = config.users[0] + return client.start(user.username, user.password) + } + + override fun close() { + log.info("Stopping node '${config.commonName}'") + node.destroy() + if (!node.waitFor(60, SECONDS)) { + log.warn("Node '${config.commonName}' has not shutdown correctly") + node.destroyForcibly() + } + + log.info("Deleting Artemis directories, because they're large!") + nodeDir.resolve("artemis").toFile().deleteRecursively() + } + + class Factory(val nodesDir: Path) { + init { + assertTrue(nodesDir.toFile().forceDirectory(), "Directory '$nodesDir' does not exist") + } + + fun create(config: NodeConfig): NodeProcess { + val nodeDir = Files.createTempDirectory(nodesDir, config.commonName) + log.info("Node directory: {}", nodeDir) + + val confFile = nodeDir.resolve("node.conf").toFile() + confFile.writeText(config.toText()) + + val process = startNode(nodeDir) + val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + val user = config.users[0] + + val setupExecutor = Executors.newSingleThreadScheduledExecutor() + try { + setupExecutor.scheduleWithFixedDelay({ + try { + if (!process.isAlive) { + log.error("Node '${config.commonName}' has died.") + return@scheduleWithFixedDelay + } + val conn = client.start(user.username, user.password) + conn.close() + + // Cancel the "setup" task now that we've created the RPC client. + setupExecutor.shutdown() + } catch (e: Exception) { + log.warn("Node '{}' not ready yet (Error: {})", config.commonName, e.message) + } + }, 5, 1, SECONDS) + + val setupOK = setupExecutor.awaitTermination(120, SECONDS) + assertTrue(setupOK && process.isAlive, "Failed to create RPC connection") + } catch (e: Exception) { + process.destroyForcibly() + throw e + } finally { + setupExecutor.shutdownNow() + } + + return NodeProcess(config, nodeDir, process, client) + } + + private fun startNode(nodeDir: Path): Process { + val builder = ProcessBuilder() + .command(javaPath.toString(), "-jar", corda.path) + .directory(nodeDir.toFile()) + + builder.environment().putAll(mapOf( + "CAPSULE_CACHE_DIR" to capsuleDir.toString() + )) + + return builder.start() + } + } +} + +private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() + diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt new file mode 100644 index 0000000000..f7863c4029 --- /dev/null +++ b/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/StandaloneCordaRPClientTest.kt @@ -0,0 +1,158 @@ +package net.corda.kotlin.rpc + +import java.io.FilterInputStream +import java.io.InputStream +import java.nio.file.Path +import java.nio.file.Paths +import java.time.Duration.ofSeconds +import java.util.Currency +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.* +import net.corda.client.rpc.CordaRPCConnection +import net.corda.client.rpc.notUsed +import net.corda.core.contracts.* +import net.corda.core.getOrThrow +import net.corda.core.identity.Party +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.messaging.startFlow +import net.corda.core.messaging.startTrackedFlow +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.sizedInputStreamAndHash +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.loggerFor +import net.corda.flows.CashIssueFlow +import net.corda.nodeapi.User +import org.junit.After +import org.junit.Before +import org.junit.Test + +class StandaloneCordaRPClientTest { + private companion object { + val log = loggerFor() + val buildDir: Path = Paths.get(System.getProperty("build.dir")) + val nodesDir: Path = buildDir.resolve("nodes") + val user = User("user1", "test", permissions = setOf("ALL")) + val factory = NodeProcess.Factory(nodesDir) + val port = AtomicInteger(15000) + const val attachmentSize = 2116 + const val timeout = 60L + } + + private lateinit var notary: NodeProcess + private lateinit var rpcProxy: CordaRPCOps + private lateinit var connection: CordaRPCConnection + private lateinit var notaryIdentity: Party + + private val notaryConfig = NodeConfig( + party = DUMMY_NOTARY, + p2pPort = port.andIncrement, + rpcPort = port.andIncrement, + webPort = port.andIncrement, + extraServices = listOf("corda.notary.validating"), + users = listOf(user) + ) + + @Before + fun setUp() { + notary = factory.create(notaryConfig) + connection = notary.connect() + rpcProxy = connection.proxy + notaryIdentity = fetchNotaryIdentity() + } + + @After + fun done() { + try { + connection.close() + } finally { + notary.close() + } + } + + @Test + fun `test attachment upload`() { + val attachment = sizedInputStreamAndHash(attachmentSize) + assertFalse(rpcProxy.attachmentExists(attachment.sha256)) + val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) } + assertEquals(id, attachment.sha256, "Attachment has incorrect SHA256 hash") + } + + @Test + fun `test starting flow`() { + rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + } + + @Test + fun `test starting tracked flow`() { + var trackCount = 0 + val handle = rpcProxy.startTrackedFlow( + ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity + ) + handle.progress.subscribe { msg -> + log.info("Flow>> $msg") + ++trackCount + } + handle.returnValue.getOrThrow(ofSeconds(timeout)) + assertNotEquals(0, trackCount) + } + + @Test + fun `test network map`() { + assertEquals(DUMMY_NOTARY.name, notaryIdentity.name) + } + + @Test + fun `test state machines`() { + val (stateMachines, updates) = rpcProxy.stateMachinesAndUpdates() + assertEquals(0, stateMachines.size) + + var updateCount = 0 + updates.subscribe { update -> + if (update is StateMachineUpdate.Added) { + log.info("StateMachine>> Id=${update.id}") + ++updateCount + } + } + + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + assertEquals(1, updateCount) + } + + @Test + fun `test vault`() { + val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates() + assertEquals(0, vault.size) + + var updateCount = 0 + vaultUpdates.subscribe { update -> + log.info("Vault>> FlowId=${update.flowId}") + ++updateCount + } + + // Now issue some cash + rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) + .returnValue.getOrThrow(ofSeconds(timeout)) + assertNotEquals(0, updateCount) + + // Check that this cash exists in the vault + val cashBalance = rpcProxy.getCashBalances() + log.info("Cash Balances: $cashBalance") + assertEquals(1, cashBalance.size) + assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) + } + + + private fun fetchNotaryIdentity(): Party { + val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates() + nodeUpdates.notUsed() + assertEquals(1, nodeInfo.size) + return nodeInfo[0].legalIdentity + } + + // This InputStream cannot have been whitelisted. + private class WrapperStream(input: InputStream) : FilterInputStream(input) +} diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt index 6139ad79fb..20026ab7c1 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/AbstractRPCTest.kt @@ -47,8 +47,8 @@ open class AbstractRPCTest { }.get() RPCTestMode.Netty -> startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server -> - startRpcClient(server.hostAndPort, rpcUser.username, rpcUser.password, clientConfiguration).map { - TestProxy(it, { startArtemisSession(server.hostAndPort, rpcUser.username, rpcUser.password) }) + startRpcClient(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map { + TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) }) } }.get() } diff --git a/constants.properties b/constants.properties index 6312235f73..74d2be1352 100644 --- a/constants.properties +++ b/constants.properties @@ -1,4 +1,4 @@ -gradlePluginsVersion=0.12.0 +gradlePluginsVersion=0.12.1 kotlinVersion=1.1.2 guavaVersion=21.0 bouncycastleVersion=1.56 diff --git a/cordform-common/build.gradle b/cordform-common/build.gradle new file mode 100644 index 0000000000..c3c1676b23 --- /dev/null +++ b/cordform-common/build.gradle @@ -0,0 +1,15 @@ +apply plugin: 'java' +apply plugin: 'maven-publish' +apply plugin: 'net.corda.plugins.publish-utils' + +repositories { + mavenCentral() +} + +dependencies { + // TypeSafe Config: for simple and human friendly config files. + compile "com.typesafe:config:$typesafe_config_version" + + // Bouncy Castle: for X.500 distinguished name manipulation + compile "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version" +} diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformContext.java b/cordform-common/src/main/java/net/corda/cordform/CordformContext.java new file mode 100644 index 0000000000..c127392f5c --- /dev/null +++ b/cordform-common/src/main/java/net/corda/cordform/CordformContext.java @@ -0,0 +1,8 @@ +package net.corda.cordform; + +import org.bouncycastle.asn1.x500.X500Name; +import java.nio.file.Path; + +public interface CordformContext { + Path baseDirectory(X500Name nodeName); +} diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java b/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java new file mode 100644 index 0000000000..85a171f8fa --- /dev/null +++ b/cordform-common/src/main/java/net/corda/cordform/CordformDefinition.java @@ -0,0 +1,27 @@ +package net.corda.cordform; + +import org.bouncycastle.asn1.x500.X500Name; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.function.Consumer; + +public abstract class CordformDefinition { + public final Path driverDirectory; + public final ArrayList> nodeConfigurers = new ArrayList<>(); + public final X500Name networkMapNodeName; + + public CordformDefinition(Path driverDirectory, X500Name networkMapNodeName) { + this.driverDirectory = driverDirectory; + this.networkMapNodeName = networkMapNodeName; + } + + public void addNode(Consumer configurer) { + nodeConfigurers.add(configurer); + } + + /** + * Make arbitrary changes to the node directories before they are started. + * @param context Lookup of node directory by node name. + */ + public abstract void setup(CordformContext context); +} diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java new file mode 100644 index 0000000000..d8160d70cc --- /dev/null +++ b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -0,0 +1,92 @@ +package net.corda.cordform; + +import static java.util.Collections.emptyList; +import com.typesafe.config.Config; +import com.typesafe.config.ConfigFactory; +import com.typesafe.config.ConfigValueFactory; +import java.util.List; +import java.util.Map; + +public class CordformNode { + protected static final String DEFAULT_HOST = "localhost"; + + /** + * Name of the node. + */ + private String name; + + public String getName() { + return name; + } + + /** + * A list of advertised services ID strings. + */ + public List advertisedServices = emptyList(); + + /** + * If running a distributed notary, a list of node addresses for joining the Raft cluster + */ + public List notaryClusterAddresses = emptyList(); + /** + * Set the RPC users for this node. This configuration block allows arbitrary configuration. + * The recommended current structure is: + * [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]] + * The above is a list to a map of keys to values using Groovy map and list shorthands. + * + * Incorrect configurations will not cause a DSL error. + */ + public List> rpcUsers = emptyList(); + + protected Config config = ConfigFactory.empty(); + + public Config getConfig() { + return config; + } + + /** + * Set the name of the node. + * + * @param name The node name. + */ + public void name(String name) { + this.name = name; + config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name)); + } + + /** + * Set the nearest city to the node. + * + * @param nearestCity The name of the nearest city to the node. + */ + public void nearestCity(String nearestCity) { + config = config.withValue("nearestCity", ConfigValueFactory.fromAnyRef(nearestCity)); + } + + /** + * Set the Artemis P2P port for this node. + * + * @param p2pPort The Artemis messaging queue port. + */ + public void p2pPort(Integer p2pPort) { + config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort)); + } + + /** + * Set the Artemis RPC port for this node. + * + * @param rpcPort The Artemis RPC queue port. + */ + public void rpcPort(Integer rpcPort) { + config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort)); + } + + /** + * Set the port which to bind the Copycat (Raft) node to + * + * @param notaryPort The Raft port. + */ + public void notaryNodePort(Integer notaryPort) { + config = config.withValue("notaryNodeAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + notaryPort)); + } +} diff --git a/core/build.gradle b/core/build.gradle index 0e24a0eb52..f0029a6672 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -63,7 +63,7 @@ dependencies { compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" // Java ed25519 implementation. See https://github.com/str4d/ed25519-java/ - compile 'net.i2p.crypto:eddsa:0.2.0' + compile "net.i2p.crypto:eddsa:$eddsa_version" // Bouncy castle support needed for X509 certificate manipulation compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 6bdb45d83e..c4218c1784 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -9,6 +9,7 @@ import com.google.common.util.concurrent.* import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.sha256 +import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable import org.slf4j.Logger import rx.Observable @@ -32,13 +33,7 @@ import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import kotlin.collections.Iterable import kotlin.collections.LinkedHashMap -import kotlin.collections.List -import kotlin.collections.filter -import kotlin.collections.firstOrNull -import kotlin.collections.fold -import kotlin.collections.forEach import kotlin.concurrent.withLock import kotlin.reflect.KProperty @@ -113,8 +108,17 @@ infix fun ListenableFuture.success(body: (T) -> Unit): ListenableFuture ListenableFuture.failure(body: (Throwable) -> Unit): ListenableFuture = apply { failure(RunOnCallerThread, body) } @Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe. infix fun ListenableFuture.map(mapper: (F) -> T): ListenableFuture = Futures.transform(this, { (mapper as (F?) -> T)(it) }) - infix fun ListenableFuture.flatMap(mapper: (F) -> ListenableFuture): ListenableFuture = Futures.transformAsync(this) { mapper(it!!) } + +inline fun Collection.mapToArray(transform: (T) -> R) = run { + val iterator = iterator() + var expected = 0 + Array(size) { + expected++ == it || throw UnsupportedOperationException("Array constructor is non-sequential!") + transform(iterator.next()) + } +} + /** Executes the given block and sets the future to either the result, or any exception that was thrown. */ inline fun SettableFuture.catch(block: () -> T) { try { @@ -136,7 +140,8 @@ fun ListenableFuture.toObservable(): Observable { } /** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */ -operator fun Path.div(other: String): Path = resolve(other) +operator fun Path.div(other: String) = resolve(other) +operator fun String.div(other: String) = Paths.get(this) / other fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs) fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs) @@ -271,7 +276,7 @@ class ThreadBox(val content: T, val lock: ReentrantLock = ReentrantLock() * We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization. */ @CordaSerializable -abstract class RetryableException(message: String) : Exception(message) +abstract class RetryableException(message: String) : FlowException(message) /** * A simple wrapper that enables the use of Kotlin's "val x by TransientProperty { ... }" syntax. Such a property diff --git a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt index 5b62e342f9..437dcb4e96 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -56,6 +56,7 @@ infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantit //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// object Requirements { + /** Throws [IllegalArgumentException] if the given expression evaluates to false. */ @Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI. infix inline fun String.using(expr: Boolean) { if (!expr) throw IllegalArgumentException("Failed requirement: $this") @@ -93,13 +94,14 @@ inline fun Collection filter { if (parties == null) true else it.signingParties.containsAll(parties) }. map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } +/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ inline fun Collection>.requireSingleCommand() = try { select().single() } catch (e: NoSuchElementException) { throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message. } -// For Java +/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */ fun Collection>.requireSingleCommand(klass: Class) = mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject else null }.single() @@ -115,7 +117,7 @@ inline fun verifyMoveCommand(inputs: List() val keysThatSigned = command.signers.toSet() requireThat { diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt index f7116222ac..31e85f859c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/DummyContract.kt @@ -1,9 +1,9 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder -import java.security.PublicKey // The dummy contract doesn't do anything useful. It exists for testing purposes. @@ -14,12 +14,12 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur val magicNumber: Int } - data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State { + data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: AbstractParty) : OwnableState, State { override val contract = DUMMY_PROGRAM_ID - override val participants: List + override val participants: List get() = listOf(owner) - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) } /** @@ -28,9 +28,9 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur * in a different field, however this is a good example of a contract with multiple states. */ data class MultiOwnerState(override val magicNumber: Int = 0, - val owners: List) : ContractState, State { + val owners: List) : ContractState, State { override val contract = DUMMY_PROGRAM_ID - override val participants: List get() = owners + override val participants: List get() = owners } interface Commands : CommandData { @@ -47,22 +47,22 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder { val owners = listOf(owner) + otherOwners return if (owners.size == 1) { - val state = SingleOwnerState(magicNumber, owners.first().party.owningKey) + val state = SingleOwnerState(magicNumber, owners.first().party) TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey)) } else { - val state = MultiOwnerState(magicNumber, owners.map { it.party.owningKey }) + val state = MultiOwnerState(magicNumber, owners.map { it.party }) TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey })) } } - fun move(prior: StateAndRef, newOwner: PublicKey) = move(listOf(prior), newOwner) - fun move(priors: List>, newOwner: PublicKey): TransactionBuilder { + fun move(prior: StateAndRef, newOwner: AbstractParty) = move(listOf(prior), newOwner) + fun move(priors: List>, newOwner: AbstractParty): TransactionBuilder { require(priors.isNotEmpty()) val priorState = priors[0].state.data val (cmd, state) = priorState.withNewOwner(newOwner) return TransactionType.General.Builder(notary = priors[0].state.notary).withItems( /* INPUTS */ *priors.toTypedArray(), - /* COMMAND */ Command(cmd, priorState.owner), + /* COMMAND */ Command(cmd, priorState.owner.owningKey), /* OUTPUT */ state ) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt index 2fcc633144..a0c4386236 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/DummyContractV2.kt @@ -1,9 +1,9 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.transactions.WireTransaction import net.corda.flows.ContractUpgradeFlow -import java.security.PublicKey // The dummy contract doesn't do anything useful. It exists for testing purposes. val DUMMY_V2_PROGRAM_ID = DummyContractV2() @@ -15,9 +15,9 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2() class DummyContractV2 : UpgradedContract { override val legacyContract = DummyContract::class.java - data class State(val magicNumber: Int = 0, val owners: List) : ContractState { + data class State(val magicNumber: Int = 0, val owners: List) : ContractState { override val contract = DUMMY_V2_PROGRAM_ID - override val participants: List = owners + override val participants: List = owners } interface Commands : CommandData { @@ -44,16 +44,16 @@ class DummyContractV2 : UpgradedContract): Pair> { + fun generateUpgradeFromV1(vararg states: StateAndRef): Pair> { val notary = states.map { it.state.notary }.single() require(states.isNotEmpty()) - val signees = states.flatMap { it.state.data.participants }.toSet() + val signees: Set = states.flatMap { it.state.data.participants }.distinct().toSet() return Pair(TransactionType.General.Builder(notary).apply { states.forEach { addInputState(it) addOutputState(upgrade(it.state.data)) - addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.toList()) + addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList()) } }.toWireTransaction(), signees) } diff --git a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt b/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt index 0eb8d4555f..1498b8c379 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/DummyState.kt @@ -1,12 +1,12 @@ package net.corda.core.contracts -import java.security.PublicKey +import net.corda.core.identity.AbstractParty /** * Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. */ data class DummyState(val magicNumber: Int = 0) : ContractState { override val contract = DUMMY_PROGRAM_ID - override val participants: List + override val participants: List get() = emptyList() } diff --git a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt index da4dca8a32..bf6d856d97 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/FungibleAsset.kt @@ -1,12 +1,8 @@ package net.corda.core.contracts -import net.corda.core.identity.Party import net.corda.core.flows.FlowException -import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.loggerFor -import net.corda.core.utilities.trace +import net.corda.core.identity.AbstractParty import java.security.PublicKey -import java.util.* class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing") @@ -32,9 +28,9 @@ interface FungibleAsset : OwnableState { */ val exitKeys: Collection /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey + override val owner: AbstractParty - fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset // Just for grouping interface Commands : CommandData { diff --git a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt index 6ea81ca9a5..5f687b2fb4 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.clauses.Clause import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRefFactory +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType @@ -114,7 +115,7 @@ interface ContractState { * The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants * list should just contain the owner. */ - val participants: List + val participants: List } /** @@ -174,10 +175,10 @@ fun Amount>.withoutIssuer(): Amount = Amount(quantity, to */ interface OwnableState : ContractState { /** There must be a MoveCommand signed by this key to claim the amount */ - val owner: PublicKey + val owner: AbstractParty /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ - fun withNewOwner(newOwner: PublicKey): Pair + fun withNewOwner(newOwner: AbstractParty): Pair } /** Something which is scheduled to happen at a point in time */ @@ -280,7 +281,7 @@ interface DealState : LinearState { * separate process exchange certificates to ascertain identities. Thus decoupling identities from * [ContractState]s. * */ - val parties: List + val parties: List /** * Generate a partial transaction representing an agreement (command) to this deal, allowing a general @@ -343,9 +344,7 @@ inline fun Iterable>.filt * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party. */ @CordaSerializable -data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) { - constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference) - +data class PartyAndReference(val party: AbstractParty, val reference: OpaqueBytes) { override fun toString() = "$party$reference" } @@ -411,7 +410,12 @@ data class AuthenticatedObject( * between (after, before). */ @CordaSerializable -data class Timestamp(val after: Instant?, val before: Instant?) { +data class Timestamp( + /** The time at which this transaction is said to have occurred is after this moment */ + val after: Instant?, + /** The time at which this transaction is said to have occurred is before this moment */ + val before: Instant? +) { init { if (after == null && before == null) throw IllegalArgumentException("At least one of before/after must be specified") diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt index bac9fbde9d..e4d343a3c6 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -2,6 +2,7 @@ package net.corda.core.contracts import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable +import net.corda.core.serialization.DeserializeAsKotlinObjectDef import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -60,7 +61,7 @@ sealed class TransactionType { abstract fun verifyTransaction(tx: LedgerTransaction) /** A general transaction type where transaction validity is determined by custom contract code */ - object General : TransactionType() { + object General : TransactionType(), DeserializeAsKotlinObjectDef { /** Just uses the default [TransactionBuilder] with no special logic */ class Builder(notary: Party?) : TransactionBuilder(General, notary) @@ -140,14 +141,14 @@ sealed class TransactionType { * A special transaction type for reassigning a notary for a state. Validation does not involve running * any contract code, it just checks that the states are unmodified apart from the notary field. */ - object NotaryChange : TransactionType() { + object NotaryChange : TransactionType(), DeserializeAsKotlinObjectDef { /** * A transaction builder that automatically sets the transaction type to [NotaryChange] * and adds the list of participants to the signers set for every input state. */ class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) { override fun addInputState(stateAndRef: StateAndRef<*>) { - signers.addAll(stateAndRef.state.data.participants) + signers.addAll(stateAndRef.state.data.participants.map { it.owningKey }) super.addInputState(stateAndRef) } } @@ -170,6 +171,6 @@ sealed class TransactionType { } } - override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet() + override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet() } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt index 69214c380b..328af22603 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeSignature.kt @@ -1,6 +1,8 @@ package net.corda.core.crypto import net.corda.core.serialization.deserialize +import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.x509.AlgorithmIdentifier import java.io.ByteArrayOutputStream import java.security.* import java.security.spec.AlgorithmParameterSpec @@ -10,7 +12,12 @@ import java.security.spec.AlgorithmParameterSpec */ class CompositeSignature : Signature(ALGORITHM) { companion object { - val ALGORITHM = "X-Corda-CompositeSig" + val ALGORITHM = "2.25.30086077608615255153862931087626791003" + // UUID-based OID + // TODO: Register for an OID space and issue our own shorter OID + val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM)) + + fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap()) } private var signatureState: State? = null diff --git a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt index e102c2bd99..6e1a2fec15 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -21,13 +21,20 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey +import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider +import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey +import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec +import sun.security.pkcs.PKCS8Key +import sun.security.util.DerValue +import sun.security.x509.X509Key import java.math.BigInteger import java.security.* import java.security.KeyFactory @@ -140,6 +147,10 @@ object Crypto { SPHINCS256_SHA256 ).associateBy { it.schemeCodeName } + // We need to group signature schemes per algorithm, so to quickly identify them during decoding. + // Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256. + private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName } + // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // that could cause unexpected and suspicious behaviour. // i.e. if someone removes a Provider and then he/she adds a new one with the same name. @@ -167,37 +178,20 @@ object Crypto { * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested signature scheme is not supported. */ - fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName") + fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") /** * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. * This function is usually called when requiring to verify signatures and the signing schemes must be defined. - * Note that only the Corda platform standard schemes are supported (see [Crypto]). - * Note that we always need to add an additional if-else statement when there are signature schemes - * with the same algorithmName, but with different parameters (e.g. now there are two ECDSA schemes, each using its own curve). + * For the supported signature schemes see [Crypto]. * @param key either private or public. * @return a currently supported SignatureScheme. * @throws IllegalArgumentException if the requested key type is not supported. */ fun findSignatureScheme(key: Key): SignatureScheme { - for (sig in supportedSignatureSchemes.values) { - var algorithm = key.algorithm - if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC. - if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256. - if (algorithm == sig.algorithmName) { - // If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters. - if (algorithm == "EdDSA") { - if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) { - return sig - } else break // use continue if in the future we support more than one Edwards curves. - } else if (algorithm == "ECDSA") { - if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) { - return sig - } else continue - } else return sig // it's either RSA_SHA256 or SPHINCS-256. - } - } - throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}") + val algorithm = matchingAlgorithmName(key.algorithm) + algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it } + throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format") } /** @@ -209,11 +203,16 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { - for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { + val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm) + // There are cases where the same key algorithm is applied to different signature schemes. + // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. + // In such a case, we should try and identify which of the candidate schemes is the correct one so as + // to generate the appropriate key. + for (signatureScheme in algorithmGroups[algorithm]!!) { try { - return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception. + // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. } } throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.") @@ -240,6 +239,8 @@ object Crypto { */ @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey { + if (!isSupportedSignatureScheme(signatureScheme)) + throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") try { return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { @@ -256,11 +257,16 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun decodePublicKey(encodedKey: ByteArray): PublicKey { - for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { + val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm) + // There are cases where the same key algorithm is applied to different signature schemes. + // Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. + // In such a case, we should try and identify which of the candidate schemes is the correct one so as + // to generate the appropriate key. + for (signatureScheme in algorithmGroups[algorithm]!!) { try { - return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) + return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { - // ignore it - only used to bypass the scheme that causes an exception. + // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params. } } throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.") @@ -271,7 +277,7 @@ object Crypto { * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). * @param encodedKey an X509 encoded public key. - * @throws IllegalArgumentException if the requested scheme is not supported + * @throws IllegalArgumentException if the requested scheme is not supported. * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ @@ -283,12 +289,14 @@ object Crypto { * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256). * @param encodedKey an X509 encoded public key. - * @throws IllegalArgumentException if the requested scheme is not supported + * @throws IllegalArgumentException if the requested scheme is not supported. * @throws InvalidKeySpecException if the given key specification * is inappropriate for this key factory to produce a public key. */ @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { + if (!isSupportedSignatureScheme(signatureScheme)) + throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") try { return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { @@ -334,7 +342,7 @@ object Crypto { */ @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) + if (!isSupportedSignatureScheme(signatureScheme)) throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!") @@ -414,7 +422,7 @@ object Crypto { */ @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) + if (!isSupportedSignatureScheme(signatureScheme)) throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") @@ -440,7 +448,7 @@ object Crypto { */ @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean { - if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.encoded.toBase58()} does not match the input clearData: ${publicKey.encoded.toBase58()}") + if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.toStringShort()} does not match") return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes()) } @@ -477,7 +485,7 @@ object Crypto { */ @Throws(SignatureException::class, IllegalArgumentException::class) fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) + if (!isSupportedSignatureScheme(signatureScheme)) throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) signature.initVerify(publicKey) @@ -505,7 +513,7 @@ object Crypto { @Throws(IllegalArgumentException::class) @JvmOverloads fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { - if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) + if (!isSupportedSignatureScheme(signatureScheme)) throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) if (signatureScheme.algSpec != null) @@ -547,20 +555,16 @@ object Crypto { return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv)) } - /** Check if the requested signature scheme is supported by the system. */ - fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes - - fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes - /** - * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key + * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key. */ fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair, subject: X500Name, subjectPublicKey: PublicKey, keyUsage: KeyUsage, purposes: List, - signatureScheme: SignatureScheme, validityWindow: Pair, + validityWindow: Pair, pathLength: Int? = null, subjectAlternativeName: List? = null): X509Certificate { + val signatureScheme = findSignatureScheme(issuerKeyPair.private) val provider = providerMap[signatureScheme.providerName] val serial = BigInteger.valueOf(random63BitValue()) val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } }) @@ -598,9 +602,9 @@ object Crypto { * Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks. * Point-at-infinity is not permitted as well. * @see Small subgroup and invalid-curve attacks for a more descriptive explanation on such attacks. - * We use this function on [findSignatureScheme] for a [PublicKey]; currently used for signature verification only. + * We use this function on [validatePublicKey], which is currently used for signature verification only. * Thus, as these attacks are mostly not relevant to signature verification, we should note that - * we're doing it out of an abundance of caution and specifically to proactively protect developers + * we are doing it out of an abundance of caution and specifically to proactively protect developers * against using these points as part of a DH key agreement or for use cases as yet unimagined. * This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve). * @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve. @@ -622,4 +626,69 @@ object Crypto { // return true if EdDSA publicKey is point at infinity. // For EdDSA a custom function is required as it is not supported by the I2P implementation. private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) + + /** Check if the requested [SignatureScheme] is supported by the system. */ + fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme + + // map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names. + private fun matchingAlgorithmName(algorithm: String): String { + return when (algorithm) { + "EC" -> "ECDSA" + "SPHINCS-256" -> "SPHINCS256" + "1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256. + else -> algorithm + } + } + + // validate a key, by checking its algorithmic params. + private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { + return when (key) { + is PublicKey -> validatePublicKey(signatureScheme, key) + is PrivateKey -> validatePrivateKey(signatureScheme, key) + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + // check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity). + private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean { + when (key) { + is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key) + is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + // check if a private key satisfies algorithm specs. + private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean { + when (key) { + is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec + is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec + is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size). + else -> throw IllegalArgumentException("Unsupported key type: ${key::class}") + } + } + + /** + * Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. + * This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations. + * @param key a public key. + * @return a supported implementation of the input public key. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ + fun toSupportedPublicKey(key: PublicKey): PublicKey { + return Crypto.decodePublicKey(key.encoded) + } + + /** + * Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key. + * This method is usually required to retrieve keys from JKS keystores that by default return SUN implementations. + * @param key a private key. + * @return a supported implementation of the input private key. + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ + fun toSupportedPrivateKey(key: PrivateKey): PrivateKey { + return Crypto.decodePrivateKey(key.encoded) + } } diff --git a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt index 4a51cb10c1..0090e89014 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -2,10 +2,11 @@ package net.corda.core.crypto +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes -import net.i2p.crypto.eddsa.EdDSAPublicKey import java.math.BigInteger import net.corda.core.utilities.SgxSupport import java.security.* @@ -19,6 +20,8 @@ object NullPublicKey : PublicKey, Comparable { override fun toString() = "NULL_KEY" } +val NULL_PARTY = AnonymousParty(NullPublicKey) + // TODO: Clean up this duplication between Null and Dummy public key @CordaSerializable class DummyPublicKey(val s: String) : PublicKey, Comparable { @@ -69,11 +72,9 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.byte // implementation of CompositeSignature. @Throws(InvalidKeyException::class) fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable { + // Quick workaround when we have CompositeKey as Party owningKey. + if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.") val sig = sign(bytesToSign) - val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey. - is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.") - else -> party.owningKey - } return DigitalSignature.LegallyIdentifiable(party, sig.bytes) } diff --git a/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt b/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt index e07ef9d8c5..138eed3c56 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt @@ -16,10 +16,10 @@ object KeyStoreUtilities { /** * Helper method to either open an existing keystore for modification, or create a new blank keystore. - * @param keyStoreFilePath location of KeyStore file + * @param keyStoreFilePath location of KeyStore file. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened/created + * @return returns the KeyStore opened/created. */ fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore { val pass = storePassword.toCharArray() @@ -34,11 +34,11 @@ object KeyStoreUtilities { } /** - * Helper method to open an existing keystore for modification/read - * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException + * Helper method to open an existing keystore for modification/read. + * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened + * @return returns the KeyStore opened. * @throws IOException if there was an error reading the key store from the file. * @throws KeyStoreException if the password is incorrect or the key store is damaged. */ @@ -48,11 +48,11 @@ object KeyStoreUtilities { } /** - * Helper method to open an existing keystore for modification/read - * @param input stream containing a KeyStore e.g. loaded from a resource file + * Helper method to open an existing keystore for modification/read. + * @param input stream containing a KeyStore e.g. loaded from a resource file. * @param storePassword password to open the store. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @return returns the KeyStore opened + * @return returns the KeyStore opened. * @throws IOException if there was an error reading the key store from the stream. * @throws KeyStoreException if the password is incorrect or the key store is damaged. */ @@ -68,12 +68,12 @@ object KeyStoreUtilities { } /** - * Helper extension method to add, or overwrite any key data in store - * @param alias name to record the private key and certificate chain under - * @param key cryptographic key to store + * Helper extension method to add, or overwrite any key data in store. + * @param alias name to record the private key and certificate chain under. + * @param key cryptographic key to store. * @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. - * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert + * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert. */ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array) { if (containsAlias(alias)) { @@ -83,9 +83,9 @@ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain } /** - * Helper extension method to add, or overwrite any public certificate data in store - * @param alias name to record the public certificate under - * @param cert certificate to store + * Helper extension method to add, or overwrite any public certificate data in store. + * @param alias name to record the public certificate under. + * @param cert certificate to store. */ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { if (containsAlias(alias)) { @@ -96,8 +96,8 @@ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { /** - * Helper method save KeyStore to storage - * @param keyStoreFilePath the file location to save to + * Helper method save KeyStore to storage. + * @param keyStoreFilePath the file location to save to. * @param storePassword password to access the store in future. This does not have to be the same password as any keys stored, * but for SSL purposes this is recommended. */ @@ -108,29 +108,47 @@ fun KeyStore.store(out: OutputStream, password: String) = store(out, password.to /** * Extract public and private keys from a KeyStore file assuming storage alias is known. - * @param keyPassword Password to unlock the private key entries - * @param alias The name to lookup the Key and Certificate chain from - * @return The KeyPair found in the KeyStore under the specified alias + * @param alias The name to lookup the Key and Certificate chain from. + * @param keyPassword Password to unlock the private key entries. + * @return The KeyPair found in the KeyStore under the specified alias. */ -fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKey(alias, keyPassword).keyPair +fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKeyPair(alias, keyPassword).keyPair /** * Helper method to load a Certificate and KeyPair from their KeyStore. * The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys. - * @param keyPassword The password for the PrivateKey (not the store access password) * @param alias The name to search for the data. Typically if generated with the methods here this will be one of - * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above + * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above. + * @param keyPassword The password for the PrivateKey (not the store access password). */ -fun KeyStore.getCertificateAndKey(alias: String, keyPassword: String): CertificateAndKey { - val keyPass = keyPassword.toCharArray() - val key = getKey(alias, keyPass) as PrivateKey +fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair { val cert = getCertificate(alias) as X509Certificate - return CertificateAndKey(cert, KeyPair(cert.publicKey, key)) + return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword))) } /** - * Extract public X509 certificate from a KeyStore file assuming storage alias is know - * @param alias The name to lookup the Key and Certificate chain from - * @return The X509Certificate found in the KeyStore under the specified alias + * Extract public X509 certificate from a KeyStore file assuming storage alias is known. + * @param alias The name to lookup the Key and Certificate chain from. + * @return The X509Certificate found in the KeyStore under the specified alias. */ fun KeyStore.getX509Certificate(alias: String): X509Certificate = getCertificate(alias) as X509Certificate + +/** + * Extract a private key from a KeyStore file assuming storage alias is known. + * By default, a JKS keystore returns PrivateKey implementations supported by the SUN provider. + * For instance, if one imports a BouncyCastle ECC key, JKS will return a SUN ECC key implementation on getKey. + * To convert to a supported implementation, an encode->decode method is applied to the keystore's returned object. + * @param alias The name to lookup the Key. + * @param keyPassword Password to unlock the private key entries. + * @return the requested private key in supported type. + * @throws KeyStoreException if the keystore has not been initialized. + * @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found (not supported from the Keystore provider). + * @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong). + * @throws IllegalArgumentException on not supported scheme or if the given key specification + * is inappropriate for a supported key factory to produce a private key. + */ +fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey { + val keyPass = keyPassword.toCharArray() + val key = getKey(alias, keyPass) as PrivateKey + return Crypto.toSupportedPrivateKey(key) +} diff --git a/core/src/main/kotlin/net/corda/core/crypto/Party.kt b/core/src/main/kotlin/net/corda/core/crypto/Party.kt deleted file mode 100644 index 883dff284c..0000000000 --- a/core/src/main/kotlin/net/corda/core/crypto/Party.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.corda.core.crypto - -import org.bouncycastle.asn1.x500.X500Name -import java.security.PublicKey - -@Deprecated("Party has moved to identity package", ReplaceWith("net.corda.core.identity.Party")) -class Party(name: X500Name, owningKey: PublicKey) : net.corda.core.identity.Party(name, owningKey) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt index aba3573bf3..73a7192afa 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -17,11 +17,12 @@ import java.io.FileWriter import java.io.InputStream import java.net.InetAddress import java.nio.file.Path +import java.security.InvalidAlgorithmParameterException import java.security.KeyPair import java.security.KeyStore import java.security.PublicKey -import java.security.cert.CertificateFactory -import java.security.cert.X509Certificate +import java.security.cert.* +import java.time.Duration import java.time.Instant import java.time.temporal.ChronoUnit import java.util.* @@ -42,29 +43,43 @@ object X509Utilities { private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage) private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth) - private val DEFAULT_VALIDITY_WINDOW = Pair(0, 365 * 10) + private val DEFAULT_VALIDITY_WINDOW = Pair(Duration.ofMillis(0), Duration.ofDays(365 * 10)) + /** - * Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range - * @param daysBefore number of days to roll back returned start date relative to current date - * @param daysAfter number of days to roll forward returned end date relative to current date - * @param parentNotBefore if provided is used to lower bound the date interval returned - * @param parentNotAfter if provided is used to upper bound the date interval returned - * Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date - * Thus we avoid too many round trip conversions. + * Helper function to return the latest out of an instant and an optional date. */ - private fun getCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair { + private fun max(first: Instant, second: Date?): Date { + return if (second != null && second.time > first.toEpochMilli()) + second + else + Date(first.toEpochMilli()) + } + + /** + * Helper function to return the earliest out of an instant and an optional date. + */ + private fun min(first: Instant, second: Date?): Date { + return if (second != null && second.time < first.toEpochMilli()) + second + else + Date(first.toEpochMilli()) + } + + /** + * Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range. + * @param before duration to roll back returned start date relative to current date. + * @param after duration to roll forward returned end date relative to current date. + * @param parent if provided certificate whose validity should bound the date interval returned. + */ + private fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509Certificate? = null): Pair { val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS) - val notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS)).let { notBefore -> - if (parentNotBefore != null && parentNotBefore.after(notBefore)) parentNotBefore else notBefore - } - val notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS)).let { notAfter -> - if (parentNotAfter != null && parentNotAfter.after(notAfter)) parentNotAfter else notAfter - } + val notBefore = max(startOfDayUTC - before, parent?.notBefore) + val notAfter = min(startOfDayUTC + after, parent?.notAfter) return Pair(notBefore, notAfter) } /** - * Return a bogus X509 for dev purposes. + * Return a bogus X509 for dev purposes. Use [getX509Name] for something more real. */ @Deprecated("Full legal names should be specified in all configurations") fun getDevX509Name(commonName: String): X500Name { @@ -96,71 +111,106 @@ object X509Utilities { /* * Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. - * @param subject the cert Subject will be populated with the domain string + * @param subject the cert Subject will be populated with the domain string. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates + * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates. */ @JvmStatic - fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { - val keyPair = generateKeyPair(signatureScheme) + fun createSelfSignedCACert(subject: X500Name, + keyPair: KeyPair, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) - val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 2) - return CertificateAndKey(cert, keyPair) + val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2) + return CertificateAndKeyPair(cert, keyPair) } + @JvmStatic + fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair + = createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow) + /** * Create a de novo root intermediate X509 v3 CA cert and KeyPair. * @param subject subject of the generated certificate. - * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it + * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. - * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates + * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates. */ @JvmStatic - fun createIntermediateCert(subject: X500Name, ca: CertificateAndKey, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { + fun createIntermediateCert(subject: X500Name, + ca: CertificateAndKeyPair, + signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair { val keyPair = generateKeyPair(signatureScheme) val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter) - val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 1) - return CertificateAndKey(cert, keyPair) + val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) + val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1) + return CertificateAndKeyPair(cert, keyPair) } /** * Create an X509v3 certificate suitable for use in TLS roles. - * @param subject The contents to put in the subject field of the certificate - * @param publicKey The PublicKey to be wrapped in the certificate - * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate - * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes - * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes - * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. + * @param subject The contents to put in the subject field of the certificate. + * @param publicKey The PublicKey to be wrapped in the certificate. + * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate. + * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes. + * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS. * This certificate is not marked as a CA cert to be similar in nature to commercial certificates. */ @JvmStatic - fun createServerCert(subject: X500Name, publicKey: PublicKey, - ca: CertificateAndKey, - subjectAlternativeNameDomains: List, - subjectAlternativeNameIps: List, - signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, - validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { + fun createTlsServerCert(subject: X500Name, publicKey: PublicKey, + ca: CertificateAndKeyPair, + subjectAlternativeNameDomains: List, + subjectAlternativeNameIps: List, + validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { val issuer = X509CertificateHolder(ca.certificate.encoded).subject - val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter) + val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate) val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) } val ipAddresses = subjectAlternativeNameIps.filter { IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it) }.map { GeneralName(GeneralName.iPAddress, it) } - return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, signatureScheme, window, subjectAlternativeName = dnsNames + ipAddresses) + return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses) } /** - * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection - * @param x509Certificate certificate to save - * @param filename Target filename + * Build a certificate path from a trusted root certificate to a target certificate. This will always return a path + * directly from the root to the target, with no intermediate certificates (presuming that path is valid). + * + * @param rootCertAndKey trusted root certificate that will be the start of the path. + * @param targetCertAndKey certificate the path ends at. + * @param revocationEnabled whether revocation of certificates in the path should be checked. + */ + fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair, + targetCertAndKey: X509Certificate, + revocationEnabled: Boolean): CertPathBuilderResult { + val intermediateCertificates = setOf(targetCertAndKey) + val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates)) + val certPathFactory = CertPathBuilder.getInstance("PKIX") + val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null) + val certPathParameters = try { + PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply { + certificate = targetCertAndKey + }) + } catch (ex: InvalidAlgorithmParameterException) { + throw RuntimeException(ex) + }.apply { + addCertStore(certStore) + isRevocationEnabled = revocationEnabled + } + return certPathFactory.build(certPathParameters) + } + + /** + * Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection. + * @param x509Certificate certificate to save. + * @param filename Target filename. */ @JvmStatic fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) { @@ -172,9 +222,9 @@ object X509Utilities { } /** - * Helper method to load back a .pem/.cer format file copy of a certificate - * @param filename Source filename - * @return The X509Certificate that was encoded in the file + * Helper method to load back a .pem/.cer format file copy of a certificate. + * @param filename Source filename. + * @return The X509Certificate that was encoded in the file. */ @JvmStatic fun loadCertificateFromPEMFile(filename: Path): X509Certificate { @@ -186,14 +236,14 @@ object X509Utilities { } /** - * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine - * @param keyStoreFilePath KeyStore path to save output to - * @param storePassword access password for KeyStore + * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine. + * @param keyStoreFilePath KeyStore path to save output to. + * @param storePassword access password for KeyStore. * @param keyPassword PrivateKey access password for the generated keys. * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. - * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore - * @param caKeyPassword password to unlock private keys in the CA KeyStore - * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications + * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore. + * @param caKeyPassword password to unlock private keys in the CA KeyStore. + * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications. */ fun createKeystoreForSSL(keyStoreFilePath: Path, storePassword: String, @@ -203,12 +253,12 @@ object X509Utilities { commonName: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore { - val rootCA = caKeyStore.getCertificateAndKey(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) - val intermediateCA = caKeyStore.getCertificateAndKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) + val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) + val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) val serverKey = generateKeyPair(signatureScheme) val host = InetAddress.getLocalHost() - val serverCert = createServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress), signatureScheme) + val serverCert = createTlsServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress)) val keyPass = keyPassword.toCharArray() val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) @@ -228,7 +278,7 @@ object X509Utilities { /** * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an - * exception + * exception. */ @Throws(IllegalArgumentException::class) fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } @@ -238,7 +288,7 @@ fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName * adds one. */ @Throws(IllegalArgumentException::class) -fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { attr -> commonName } +fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName } /** * Rebuild the distinguished name, replacing the common name with a value generated from the provided function. @@ -267,6 +317,7 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N } val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() +val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString() val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString() class CertificateStream(val input: InputStream) { @@ -275,4 +326,4 @@ class CertificateStream(val input: InputStream) { fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate } -data class CertificateAndKey(val certificate: X509Certificate, val keyPair: KeyPair) +data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair) diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt index 7b4ff2e6ce..3f9996c081 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogicRef.kt @@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable * the flow to run at the scheduled time. */ interface FlowLogicRefFactory { - fun create(type: Class>, vararg args: Any?): FlowLogicRef + fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef } @CordaSerializable -class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") +class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException( + "${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") /** * A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox. diff --git a/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt b/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt new file mode 100644 index 0000000000..729299ee1a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt @@ -0,0 +1,14 @@ +package net.corda.core.flows + +import java.lang.annotation.Inherited +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is schedulable and is designed to be invoked by a [net.corda.core.contracts.SchedulableState] + * must have this annotation. If it's missing [FlowLogicRefFactory.create] will throw an exception when it comes time + * to schedule the next activity in [net.corda.core.contracts.SchedulableState.nextScheduledActivity]. + */ +@Target(CLASS) +@Inherited +@MustBeDocumented +annotation class SchedulableFlow \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt new file mode 100644 index 0000000000..6eafd3d699 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt @@ -0,0 +1,15 @@ +package net.corda.core.flows + +import java.lang.annotation.Inherited +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Any [FlowLogic] which is to be started by the RPC interface ([net.corda.core.messaging.CordaRPCOps.startFlowDynamic] + * and [net.corda.core.messaging.CordaRPCOps.startTrackedFlowDynamic]) must have this annotation. If it's missing the + * flow will not be allowed to start and an exception will be thrown. + */ +@Target(CLASS) +@Inherited +@MustBeDocumented +// TODO Consider a different name, something along the lines of SchedulableFlow +annotation class StartableByRPC \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt index 9f90a5267a..6b285c3698 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -16,7 +16,6 @@ abstract class AbstractParty(val owningKey: PublicKey) { override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey override fun hashCode(): Int = owningKey.hashCode() - abstract fun toAnonymous(): AnonymousParty abstract fun nameOrNull(): X500Name? abstract fun ref(bytes: OpaqueBytes): PartyAndReference diff --git a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt index 81c04846cc..dc1ec16f58 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AnonymousParty.kt @@ -18,5 +18,4 @@ class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) { override fun nameOrNull(): X500Name? = null override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) - override fun toAnonymous() = this } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/identity/Party.kt b/core/src/main/kotlin/net/corda/core/identity/Party.kt index 1016bb4352..b94f266b8c 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -1,6 +1,7 @@ package net.corda.core.identity import net.corda.core.contracts.PartyAndReference +import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.serialization.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name @@ -25,11 +26,10 @@ import java.security.PublicKey * * @see CompositeKey */ -// TODO: Remove "open" from [Party] once deprecated crypto.Party class is removed -open class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { - override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey) - override fun toString() = "${owningKey.toBase58String()} ($name)" +class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { + constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public) + override fun toString() = name.toString() override fun nameOrNull(): X500Name? = name - override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes) + override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) } diff --git a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt index d5c4a02de3..190f2456a0 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps { fun networkMapUpdates(): Pair, Observable> /** - * Start the given flow with the given arguments. + * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle /** * Start the given flow with the given arguments, returning an [Observable] with a single observation of the - * result of running the flow. + * result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. */ @RPCReturnsObservables fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle diff --git a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt index e2ed48355b..6d73439665 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -8,42 +8,38 @@ import java.util.function.Function * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file * to extend a Corda node with additional application services. */ -abstract class CordaPluginRegistry( - /** - * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should - * potentially be able to live in a process separate from the node itself. - */ - open val webApis: List> = emptyList(), +abstract class CordaPluginRegistry { + /** + * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should + * potentially be able to live in a process separate from the node itself. + */ + open val webApis: List> get() = emptyList() - /** - * Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. - * Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can - * be specified with: javaClass.getResource("").toExternalForm() - */ - open val staticServeDirs: Map = emptyMap(), + /** + * Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. + * Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can + * be specified with: javaClass.getResource("").toExternalForm() + */ + open val staticServeDirs: Map get() = emptyMap() - /** - * A Map with an entry for each consumed Flow used by the webAPIs. - * The key of each map entry should contain the FlowLogic class name. - * The associated map values are the union of all concrete class names passed to the Flow constructor. - * Standard java.lang.* and kotlin.* types do not need to be included explicitly. - * This is used to extend the white listed Flows that can be initiated from the ServiceHub invokeFlowAsync method. - */ - open val requiredFlows: Map> = emptyMap(), + @Suppress("unused") + @Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " + + "@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR) + open val requiredFlows: Map> get() = emptyMap() + + /** + * List of lambdas constructing additional long lived services to be hosted within the node. + * They expect a single [PluginServiceHub] parameter as input. + * The [PluginServiceHub] will be fully constructed before the plugin service is created and will + * allow access to the Flow factory and Flow initiation entry points there. + */ + open val servicePlugins: List> get() = emptyList() - /** - * List of lambdas constructing additional long lived services to be hosted within the node. - * They expect a single [PluginServiceHub] parameter as input. - * The [PluginServiceHub] will be fully constructed before the plugin service is created and will - * allow access to the Flow factory and Flow initiation entry points there. - */ - open val servicePlugins: List> = emptyList() -) { /** * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. * - * For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by - * adding the @CordaSerializable annotation or via this method. + * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that + * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method. ** * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. */ diff --git a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt index c32c6937d1..5e589834dd 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -1,10 +1,11 @@ package net.corda.core.node import net.corda.core.contracts.* -import net.corda.core.crypto.keys +import net.corda.core.crypto.DigitalSignature import net.corda.core.node.services.* import net.corda.core.transactions.SignedTransaction -import java.security.KeyPair +import net.corda.core.transactions.TransactionBuilder +import java.security.PublicKey import java.time.Clock /** @@ -82,23 +83,107 @@ interface ServiceHub : ServicesForResolution { } /** - * Helper property to shorten code for fetching the Node's KeyPair associated with the - * public legalIdentity Party from the key management service. + * Helper property to shorten code for fetching the the [PublicKey] portion of the + * Node's primary signing identity. * Typical use is during signing in flows and for unit test signing. - * - * TODO: legalIdentity can now be composed of multiple keys, should we return a list of keyPairs here? Right now - * the logic assumes the legal identity has a composite key with only one node + * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService + * the matching [PrivateKey] will be looked up internally and used to sign. + * If the key is actually a CompositeKey, the first leaf key hosted on this node + * will be used to create the signature. */ - val legalIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.legalIdentity.owningKey.keys) + val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentity.owningKey /** - * Helper property to shorten code for fetching the Node's KeyPair associated with the - * public notaryIdentity Party from the key management service. It is assumed that this is only - * used in contexts where the Node knows it is hosting a Notary Service. Otherwise, it will throw - * an IllegalArgumentException. + * Helper property to shorten code for fetching the the [PublicKey] portion of the + * Node's Notary signing identity. It is required that the Node hosts a notary service, + * otherwise an IllegalArgumentException will be thrown. * Typical use is during signing in flows and for unit test signing. - * - * TODO: same problem as with legalIdentityKey. + * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService + * the matching [PrivateKey] will be looked up internally and used to sign. + * If the key is actually a [CompositeKey], the first leaf key hosted on this node + * will be used to create the signature. */ - val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey.keys) -} + val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey + + /** + * Helper method to construct an initial partially signed transaction from a [TransactionBuilder] + * using keys stored inside the node. + * @param builder The [TransactionBuilder] to seal with the node's signature. + * Any existing signatures on the builder will be preserved. + * @param publicKey The [PublicKey] matched to the internal [PrivateKey] to use in signing this transaction. + * If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node + * to sign with. + * @return Returns a SignedTransaction with the new node signature attached. + */ + fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey): SignedTransaction { + val sig = keyManagementService.sign(builder.toWireTransaction().id.bytes, publicKey) + builder.addSignatureUnchecked(sig) + return builder.toSignedTransaction(false) + } + + + /** + * Helper method to construct an initial partially signed transaction from a TransactionBuilder + * using the default identity key contained in the node. + * @param builder The TransactionBuilder to seal with the node's signature. + * Any existing signatures on the builder will be preserved. + * @return Returns a SignedTransaction with the new node signature attached. + */ + fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey) + + + /** + * Helper method to construct an initial partially signed transaction from a [TransactionBuilder] + * using a set of keys all held in this node. + * @param builder The [TransactionBuilder] to seal with the node's signature. + * Any existing signatures on the builder will be preserved. + * @param signingPubKeys A list of [PublicKeys] used to lookup the matching [PrivateKey] and sign. + * @throws IllegalArgumentException is thrown if any keys are unavailable locally. + * @return Returns a [SignedTransaction] with the new node signature attached. + */ + fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: List): SignedTransaction { + var stx: SignedTransaction? = null + for (pubKey in signingPubKeys) { + stx = if (stx == null) { + signInitialTransaction(builder, pubKey) + } else { + addSignature(stx, pubKey) + } + } + return stx!! + } + + /** + * Helper method to create an additional signature for an existing (partially) [SignedTransaction]. + * @param signedTransaction The [SignedTransaction] to which the signature will apply. + * @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. + * If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. + * @return The [DigitalSignature.WithKey] generated by signing with the internally held [PrivateKey]. + */ + fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey = keyManagementService.sign(signedTransaction.id.bytes, publicKey) + + /** + * Helper method to create an additional signature for an existing (partially) SignedTransaction + * using the default identity signing key of the node. + * @param signedTransaction The SignedTransaction to which the signature will apply. + * @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey. + */ + fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey = createSignature(signedTransaction, legalIdentityKey) + + /** + * Helper method to append an additional signature to an existing (partially) [SignedTransaction]. + * @param signedTransaction The [SignedTransaction] to which the signature will be added. + * @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. + * If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. + * @return A new [SignedTransaction] with the addition of the new signature. + */ + fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction = signedTransaction + createSignature(signedTransaction, publicKey) + + /** + * Helper method to ap-pend an additional signature for an existing (partially) [SignedTransaction] + * using the default identity signing key of the node. + * @param signedTransaction The [SignedTransaction] to which the signature will be added. + * @return A new [SignedTransaction] with the addition of the new signature. + */ + fun addSignature(signedTransaction: SignedTransaction): SignedTransaction = addSignature(signedTransaction, legalIdentityKey) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt index 61fb00b6b3..4dafa52ef0 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/IdentityService.kt @@ -1,10 +1,13 @@ package net.corda.core.node.services import net.corda.core.contracts.PartyAndReference +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate /** * An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports @@ -14,6 +17,29 @@ import java.security.PublicKey interface IdentityService { fun registerIdentity(party: Party) + /** + * Verify and then store the certificates proving that an anonymous party's key is owned by the given full + * party. + * + * @param trustedRoot trusted root certificate, typically the R3 master signing certificate. + * @param anonymousParty an anonymised party belonging to the legal entity. + * @param path certificate path from the trusted root to the anonymised party. + * @throws IllegalArgumentException if the chain does not link the two parties, or if there is already an existing + * certificate chain for the anonymous party. Anonymous parties must always resolve to a single owning party. + */ + // TODO: Move this into internal identity service once available + @Throws(IllegalArgumentException::class) + fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) + + /** + * Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with + * the anonymous party and resolving it back to the given full party. + * + * @throws IllegalStateException if the anonymous party is not owned by the full party. + */ + @Throws(IllegalStateException::class) + fun assertOwnership(party: Party, anonymousParty: AnonymousParty) + /** * Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be * used in preference where possible. @@ -29,6 +55,13 @@ interface IdentityService { fun partyFromName(name: String): Party? fun partyFromX500Name(principal: X500Name): Party? - fun partyFromAnonymous(party: AnonymousParty): Party? + fun partyFromAnonymous(party: AbstractParty): Party? fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + + /** + * Get the certificate chain showing an anonymous party is owned by the given party. + */ + fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? + + class UnknownAnonymousPartyException(msg: String) : Exception(msg) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/Services.kt b/core/src/main/kotlin/net/corda/core/node/services/Services.kt index c29f534a2e..6116441b0a 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/Services.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/Services.kt @@ -3,7 +3,8 @@ package net.corda.core.node.services import co.paralleluniverse.fibers.Suspendable import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -18,8 +19,6 @@ import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction import rx.Observable import java.io.InputStream -import java.security.KeyPair -import java.security.PrivateKey import java.security.PublicKey import java.time.Instant import java.util.* @@ -286,7 +285,7 @@ interface VaultService { @Suspendable fun generateSpend(tx: TransactionBuilder, amount: Amount, - to: PublicKey, + to: AbstractParty, onlyFromParties: Set? = null): Pair> // DOCSTART VaultStatesQuery @@ -371,32 +370,32 @@ class StatesNotAvailableException(override val message: String?, override val ca /** * The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example, * call out to a hardware security module that enforces various auditing and frequency-of-use requirements. - * - * The current interface is obviously not usable for those use cases: this is just where we'd put a real signing - * interface if/when one is developed. */ interface KeyManagementService { - /** Returns a snapshot of the current pubkey->privkey mapping. */ - val keys: Map + /** + * Returns a snapshot of the current signing [PublicKey]s. + * For each of these keys a [PrivateKey] is available, that can be used later for signing. + */ + val keys: Set - @Throws(IllegalStateException::class) - fun toPrivate(publicKey: PublicKey) = keys[publicKey] ?: throw IllegalStateException("No private key known for requested public key ${publicKey.toStringShort()}") + /** + * Generates a new random [KeyPair] and adds it to the internal key storage. Returns the public part of the pair. + */ + @Suspendable + fun freshKey(): PublicKey - @Throws(IllegalArgumentException::class) - fun toKeyPair(publicKey: PublicKey): KeyPair { - when (publicKey) { - is CompositeKey -> throw IllegalArgumentException("Got CompositeKey when single PublicKey expected.") - else -> return KeyPair(publicKey, toPrivate(publicKey)) - } - } - - /** Returns the first [KeyPair] matching any of the [publicKeys] */ - @Throws(IllegalArgumentException::class) - fun toKeyPair(publicKeys: Iterable) = publicKeys.first { keys.contains(it) }.let { toKeyPair(it) } - - /** Generates a new random key and adds it to the exposed map. */ - fun freshKey(): KeyPair + /** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. + * @param bytes The data to sign over using the chosen key. + * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity, + * or previously generated via the [freshKey] method. + * If the [PublicKey] is actually a [CompositeKey] the first leaf signing key hosted by the node is used. + * @throws IllegalArgumentException if the input key is not a member of [keys]. + * TODO A full [KeyManagementService] implementation needs to record activity to the [AuditService] and to limit signing to + * appropriately authorised contexts and initiating users. + */ + @Suspendable + fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey } // TODO: Move to a more appropriate location diff --git a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt index ed401c2f39..514c3384bf 100644 --- a/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt +++ b/core/src/main/kotlin/net/corda/core/schemas/PersistentTypes.kt @@ -37,9 +37,9 @@ interface QueryableState : ContractState { * @param version The version number of this instance within the family. * @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema. */ -abstract class MappedSchema(schemaFamily: Class<*>, - val version: Int, - val mappedTypes: Iterable>) { +open class MappedSchema(schemaFamily: Class<*>, + val version: Int, + val mappedTypes: Iterable>) { val name: String = schemaFamily.name override fun toString(): String = "${this.javaClass.simpleName}(name=$name, version=$version)" } diff --git a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt index b553a0f67f..825ce18a20 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -26,9 +26,12 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.objenesis.strategy.StdInstantiatorStrategy import org.slf4j.Logger +import sun.security.provider.certpath.X509CertPath import java.io.BufferedInputStream import java.io.FileInputStream import java.io.InputStream +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.util.* object DefaultKryoCustomizer { @@ -97,6 +100,12 @@ object DefaultKryoCustomizer { // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). addDefaultSerializer(InputStream::class.java, InputStreamSerializer) + register(CertPath::class.java, CertPathSerializer) + register(X509CertPath::class.java, CertPathSerializer) + // TODO: We shouldn't need to serialize raw certificates, and if we do then we need a cleaner solution + // than this mess. + val x509CertObjectClazz = Class.forName("org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject") + register(x509CertObjectClazz, X509CertificateSerializer) register(X500Name::class.java, X500NameSerializer) register(BCECPrivateKey::class.java, PrivateKeySerializer) diff --git a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt index 881046fb82..c18bfedc0b 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -31,6 +31,9 @@ import java.nio.file.Files import java.nio.file.Path import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate import java.security.spec.InvalidKeySpecException import java.time.Instant import java.util.* @@ -617,6 +620,36 @@ object X500NameSerializer : Serializer() { } } +/** + * For serialising an [CertPath] in an X.500 standard format. + */ +@ThreadSafe +object CertPathSerializer : Serializer() { + val factory = CertificateFactory.getInstance("X.509") + override fun read(kryo: Kryo, input: Input, type: Class): CertPath { + return factory.generateCertPath(input) + } + + override fun write(kryo: Kryo, output: Output, obj: CertPath) { + output.writeBytes(obj.encoded) + } +} + +/** + * For serialising an [CX509Certificate] in an X.500 standard format. + */ +@ThreadSafe +object X509CertificateSerializer : Serializer() { + val factory = CertificateFactory.getInstance("X.509") + override fun read(kryo: Kryo, input: Input, type: Class): X509Certificate { + return factory.generateCertificate(input) as X509Certificate + } + + override fun write(kryo: Kryo, output: Output, obj: X509Certificate) { + output.writeBytes(obj.encoded) + } +} + class KryoPoolWithContext(val baseKryoPool: KryoPool, val contextKey: Any, val context: Any) : KryoPool { override fun run(callback: KryoCallback): T { val kryo = borrow() diff --git a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt index 952544be36..78c25f7154 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -1,13 +1,16 @@ package net.corda.core.transactions import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.MerkleTree +import net.corda.core.crypto.MerkleTreeException +import net.corda.core.crypto.PartialMerkleTree +import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.serialize -import java.security.PublicKey import net.corda.core.serialization.withoutReferences +import java.security.PublicKey fun serializedHash(x: T): SecureHash { return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } } @@ -91,7 +94,7 @@ class FilteredLeaves( */ fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { val checkList = availableComponents.map { checkingFun(it) } - return (!checkList.isEmpty()) && checkList.all { true } + return (!checkList.isEmpty()) && checkList.all { it } } } diff --git a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt index e4358a9092..e100c89793 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -3,14 +3,12 @@ package net.corda.core.transactions import net.corda.core.contracts.AttachmentResolutionException import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.TransactionResolutionException -import net.corda.core.node.ServiceHub import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.isFulfilledBy -import net.corda.core.crypto.sign +import net.corda.core.node.ServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes -import java.security.KeyPair import java.security.PublicKey import java.security.SignatureException import java.util.* @@ -146,14 +144,5 @@ data class SignedTransaction(val txBits: SerializedBytes, @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class) fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services) - /** - * Utility to simplify the act of signing the transaction. - * - * @param keyPair the signer's public/private key pair. - * - * @return a digital signature of the transaction. - */ - fun signWithECDSA(keyPair: KeyPair) = keyPair.sign(this.id.bytes) - override fun toString(): String = "${javaClass.simpleName}(id=$id)" } diff --git a/core/src/main/kotlin/net/corda/core/utilities/LazyPool.kt b/core/src/main/kotlin/net/corda/core/utilities/LazyPool.kt index 1a1abebdca..2649924aa1 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LazyPool.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LazyPool.kt @@ -59,8 +59,10 @@ class LazyPool( * the returned iterable will be inaccurate. */ fun close(): Iterable { - lifeCycle.transition(State.STARTED, State.FINISHED) - return poolQueue + lifeCycle.justTransition(State.FINISHED) + val elements = poolQueue.toList() + poolQueue.clear() + return elements } inline fun run(withInstance: (A) -> R): R { diff --git a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt index 8dc0c45270..804dd4ed7a 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LegalNameValidator.kt @@ -24,12 +24,14 @@ fun validateLegalName(normalizedLegalName: String) { rules.forEach { it.validate(normalizedLegalName) } } +val WHITESPACE = "\\s++".toRegex() + /** * The normalize function will trim the input string, replace any multiple spaces with a single space, * and normalize the string according to NFKC normalization form. */ fun normaliseLegalName(legalName: String): String { - val trimmedLegalName = legalName.trim().replace(Regex("\\s+"), " ") + val trimmedLegalName = legalName.trim().replace(WHITESPACE, " ") return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/LifeCycle.kt b/core/src/main/kotlin/net/corda/core/utilities/LifeCycle.kt index 2a7b0c2bb3..bc73e9f51a 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/LifeCycle.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/LifeCycle.kt @@ -13,7 +13,7 @@ class LifeCycle>(initial: S) { private val lock = ReentrantReadWriteLock() private var state = initial - /** Assert that the lifecycle in the [requiredState] */ + /** Assert that the lifecycle in the [requiredState]. */ fun requireState(requiredState: S) { requireState({ "Required state to be $requiredState, was $it" }) { it == requiredState } } @@ -28,11 +28,18 @@ class LifeCycle>(initial: S) { } } - /** Transition the state from [from] to [to] */ + /** Transition the state from [from] to [to]. */ fun transition(from: S, to: S) { lock.writeLock().withLock { require(state == from) { "Required state to be $from to transition to $to, was $state" } state = to } } + + /** Transition the state to [to] without performing a current state check. */ + fun justTransition(to: S) { + lock.writeLock().withLock { + state = to + } + } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt index fccf706b97..f00941e786 100644 --- a/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/AbstractStateReplacementFlow.kt @@ -4,9 +4,11 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.isFulfilledBy import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -59,13 +61,13 @@ abstract class AbstractStateReplacementFlow { progressTracker.currentStep = SIGNING - val myKey = serviceHub.myInfo.legalIdentity.owningKey + val myKey = serviceHub.myInfo.legalIdentity val me = listOf(myKey) val signatures = if (participants == me) { getNotarySignatures(stx) } else { - collectSignatures(participants - me, stx) + collectSignatures((participants - me).map { it.owningKey }, stx) } val finalTx = stx + signatures @@ -73,7 +75,7 @@ abstract class AbstractStateReplacementFlow { return finalTx.tx.outRef(0) } - abstract protected fun assembleTx(): Pair> + abstract protected fun assembleTx(): Pair> @Suspendable private fun collectSignatures(participants: Iterable, stx: SignedTransaction): List { @@ -187,8 +189,7 @@ abstract class AbstractStateReplacementFlow { } private fun sign(stx: SignedTransaction): DigitalSignature.WithKey { - val myKey = serviceHub.legalIdentityKey - return myKey.sign(stx.id) + return serviceHub.createSignature(stx) } } } diff --git a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt new file mode 100644 index 0000000000..acfbba8541 --- /dev/null +++ b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt @@ -0,0 +1,259 @@ +package net.corda.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.isFulfilledBy +import net.corda.core.crypto.toBase58String +import net.corda.core.flows.FlowException +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.SignedTransaction +import net.corda.core.transactions.WireTransaction +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap +import java.security.PublicKey + +/** + * The [CollectSignaturesFlow] is used to automate the collection of counter-party signatures for a given transaction. + * + * You would typically use this flow after you have built a transaction with the TransactionBuilder and signed it with + * your key pair. If there are additional signatures to collect then they can be collected using this flow. Signatures + * are collected based upon the [WireTransaction.mustSign] property which contains the union of all the PublicKeys + * listed in the transaction's commands as well as a notary's public key, if required. This flow returns a + * [SignedTransaction] which can then be passed to the [FinalityFlow] for notarisation. The other side of this flow is + * the [SignTransactionFlow]. + * + * **WARNING**: This flow ONLY works with [ServiceHub.legalIdentityKey]s and WILL break if used with randomly generated + * keys by the [ServiceHub.keyManagementService]. + * + * Usage: + * + * - Call the [CollectSignaturesFlow] flow as a [subFlow] and pass it a [SignedTransaction] which has at least been + * signed by the transaction creator (and possibly an oracle, if required) + * - The flow expects that the calling node has signed the provided transaction, if not the flow will fail + * - The flow will also fail if: + * 1. The provided transaction is invalid + * 2. Any of the required signing parties cannot be found in the [ServiceHub.networkMapCache] of the initiator + * 3. If the wrong key has been used by a counterparty to sign the transaction + * 4. The counterparty rejects the provided transaction + * - The flow will return a [SignedTransaction] with all the counter-party signatures (but not the notary's!) + * - If the provided transaction has already been signed by all counter-parties then this flow simply returns the + * provided transaction without contacting any counter-parties + * - Call the [FinalityFlow] with the return value of this flow + * + * Example - issuing a multi-lateral agreement which requires N signatures: + * + * val builder = TransactionType.General.Builder(notaryRef) + * val issueCommand = Command(Agreement.Commands.Issue(), state.participants) + * + * builder.withItems(state, issueCommand) + * builder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + * + * // Transaction creator signs transaction. + * val ptx = builder.signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + * + * // Call to CollectSignaturesFlow. + * // The returned signed transaction will have all signatures appended apart from the notary's. + * val stx = subFlow(CollectSignaturesFlow(ptx)) + * + * @param partiallySignedTx Transaction to collect the remaining signatures for + */ +// TODO: AbstractStateReplacementFlow needs updating to use this flow. +// TODO: TwoPartyTradeFlow needs updating to use this flow. +// TODO: Update this flow to handle randomly generated keys when that works is complete. +class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, + override val progressTracker: ProgressTracker = tracker()): FlowLogic() { + + companion object { + object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.") + object VERIFYING : ProgressTracker.Step("Verifying collected signatures.") + + fun tracker() = ProgressTracker(COLLECTING, VERIFYING) + + // TODO: Make the progress tracker adapt to the number of counter-parties to collect from. + } + + @Suspendable override fun call(): SignedTransaction { + // TODO: Revisit when key management is properly fleshed out. + // This will break if a party uses anything other than their legalIdentityKey. + // Check the signatures which have already been provided and that the transaction is valid. + // Usually just the Initiator and possibly an oracle would have signed at this point. + val myKey = serviceHub.myInfo.legalIdentity.owningKey + val signed = partiallySignedTx.sigs.map { it.by } + val notSigned = partiallySignedTx.tx.mustSign - signed + + // One of the signatures collected so far MUST be from the initiator of this flow. + require(partiallySignedTx.sigs.any { it.by == myKey }) { + "The Initiator of CollectSignaturesFlow must have signed the transaction." + } + + // The signatures must be valid and the transaction must be valid. + partiallySignedTx.verifySignatures(*notSigned.toTypedArray()) + partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify() + + // Determine who still needs to sign. + progressTracker.currentStep = COLLECTING + val notaryKey = partiallySignedTx.tx.notary?.owningKey + // If present, we need to exclude the notary's PublicKey as the notary signature is collected separately with + // the FinalityFlow. + val unsigned = if (notaryKey != null) notSigned - notaryKey else notSigned + + // If the unsigned counter-parties list is empty then we don't need to collect any more signatures here. + if (unsigned.isEmpty()) return partiallySignedTx + + // Collect signatures from all counter-parties and append them to the partially signed transaction. + val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) } + val stx = partiallySignedTx + counterpartySignatures + + // Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures. + progressTracker.currentStep = VERIFYING + if (notaryKey != null) stx.verifySignatures(notaryKey) else stx.verifySignatures() + + return stx + } + + /** + * Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache]. + */ + @Suspendable private fun keysToParties(keys: List): List = keys.map { + // TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key. + val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it) + ?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.") + partyNode.legalIdentity + } + + /** + * Get and check the required signature. + */ + @Suspendable private fun collectSignature(counterparty: Party): DigitalSignature.WithKey { + return sendAndReceive(counterparty, partiallySignedTx).unwrap { + require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." } + it + } + } +} + +/** + * The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of + * a transaction providing the transaction: + * + * 1. Should actually be signed by the [Party] invoking this flow + * 2. Is valid as per the contracts referenced in the transaction + * 3. Has been, at least, signed by the counter-party which created it + * 4. Conforms to custom checking provided in the [checkTransaction] method of the [SignTransactionFlow] + * + * Usage: + * + * - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below) + * - Override the [checkTransaction] method to add some custom verification logic + * - Call the flow via [FlowLogic.subFlow] + * - The flow returns the fully signed transaction once it has been committed to the ledger + * + * Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see + * CollectSignaturesFlowTests.kt for further examples: + * + * class Responder(val otherParty: Party): FlowLogic() { + * @Suspendable override fun call(): SignedTransaction { + * // [SignTransactionFlow] sub-classed as a singleton object. + * val flow = object : SignTransactionFlow(otherParty) { + * @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + * val tx = stx.tx + * val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + * "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + * } + * } + * + * // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow]. + * val stx = subFlow(flow) + * + * return waitForLedgerCommit(stx.id) + * } + * } + * + * @param otherParty The counter-party which is providing you a transaction to sign. + */ +abstract class SignTransactionFlow(val otherParty: Party, + override val progressTracker: ProgressTracker = tracker()) : FlowLogic() { + + companion object { + object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.") + object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.") + object SIGNING : ProgressTracker.Step("Signing transaction proposal.") + + fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING) + } + + @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = RECEIVING + val checkedProposal = receive(otherParty).unwrap { proposal -> + progressTracker.currentStep = VERIFYING + // Check that the Responder actually needs to sign. + checkMySignatureRequired(proposal) + // Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's. + checkSignatures(proposal) + // Resolve dependencies and verify, pass in the WireTransaction as we don't have all signatures. + subFlow(ResolveTransactionsFlow(proposal.tx, otherParty)) + proposal.tx.toLedgerTransaction(serviceHub).verify() + // Perform some custom verification over the transaction. + try { + checkTransaction(proposal) + } catch(e: Exception) { + if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError) + throw FlowException(e) + else + throw e + } + // All good. Unwrap the proposal. + proposal + } + + // Sign and send back our signature to the Initiator. + progressTracker.currentStep = SIGNING + val mySignature = serviceHub.createSignature(checkedProposal) + send(otherParty, mySignature) + + // Return the fully signed transaction once it has been committed. + return waitForLedgerCommit(checkedProposal.id) + } + + @Suspendable private fun checkSignatures(stx: SignedTransaction) { + require(stx.sigs.any { it.by == otherParty.owningKey }) { + "The Initiator of CollectSignaturesFlow must have signed the transaction." + } + val signed = stx.sigs.map { it.by } + val allSigners = stx.tx.mustSign + val notSigned = allSigners - signed + stx.verifySignatures(*notSigned.toTypedArray()) + } + + /** + * The [checkTransaction] method allows the caller of this flow to provide some additional checks over the proposed + * transaction received from the counter-party. For example: + * + * - Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the + * expected type and number of inputs and outputs + * - Checking that the properties of the outputs are as you would expect. Linking into any reference data sources + * might be appropriate here + * - Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as + * potentially the transaction creator has access to some of your state references + * + * **WARNING**: If appropriate checks, such as the ones listed above, are not defined then it is likely that your + * node will sign any transaction if it conforms to the contract code in the transaction's referenced contracts. + * + * [IllegalArgumentException], [IllegalStateException] and [AssertionError] will be caught and rethrown as flow + * exceptions i.e. the other side will be given information about what exact check failed. + * + * @param stx a partially signed transaction received from your counter-party. + * @throws FlowException if the proposed transaction fails the checks. + */ + @Suspendable abstract protected fun checkTransaction(stx: SignedTransaction) + + @Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) { + // TODO: Revisit when key management is properly fleshed out. + val myKey = serviceHub.myInfo.legalIdentity.owningKey + require(myKey in stx.tx.mustSign) { + "Party is not a participant for any of the input states of transaction ${stx.id}" + } + } +} diff --git a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt index fd73271e45..e876bcabbd 100644 --- a/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/ContractUpgradeFlow.kt @@ -2,6 +2,8 @@ package net.corda.flows import net.corda.core.contracts.* import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -15,6 +17,7 @@ import java.security.PublicKey * use the new updated state for future transactions. */ @InitiatingFlow +@StartableByRPC class ContractUpgradeFlow( originalState: StateAndRef, newContractClass: Class> @@ -30,12 +33,12 @@ class ContractUpgradeFlow = input.participants.toSet() + val participantKeys: Set = input.participants.map { it.owningKey }.toSet() val keysThatSigned: Set = commandData.signers.toSet() @Suppress("UNCHECKED_CAST") val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract requireThat { - "The signing keys include all participant keys" using keysThatSigned.containsAll(participants) + "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys) "Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract) "Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass) "Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input)) @@ -51,14 +54,13 @@ class ContractUpgradeFlow> { - val stx = assembleBareTx(originalState, modification) - .signWith(serviceHub.legalIdentityKey) - .toSignedTransaction(false) + override fun assembleTx(): Pair> { + val baseTx = assembleBareTx(originalState, modification) + val stx = serviceHub.signInitialTransaction(baseTx) return stx to originalState.state.data.participants } } diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt index 0d1168f42f..ff62d89deb 100644 --- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt @@ -13,7 +13,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker /** - * Verifies the given transactions, then sends them to the named notaries. If the notary agrees that the transactions + * Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions * are acceptable then they are from that point onwards committed to the ledger, and will be written through to the * vault. Additionally they will be distributed to the parties reflected in the participants list of the states. * @@ -37,6 +37,7 @@ class FinalityFlow(val transactions: Iterable, override val progressTracker: ProgressTracker) : FlowLogic>() { constructor(transaction: SignedTransaction, extraParticipants: Set) : this(listOf(transaction), extraParticipants, tracker()) constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker()) + constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker) companion object { object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") { @@ -105,7 +106,7 @@ class FinalityFlow(val transactions: Iterable, // Calculate who is meant to see the results based on the participants involved. val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants } // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail? - val parties = keys.mapNotNull { serviceHub.identityService.partyFromKey(it) }.toSet() + val parties = keys.mapNotNull { serviceHub.identityService.partyFromAnonymous(it) }.toSet() Pair(stx, parties) } } diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index 4a80599021..8de5b096e7 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -2,6 +2,7 @@ package net.corda.flows import net.corda.core.contracts.* import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -24,11 +25,11 @@ class NotaryChangeFlow( progressTracker: ProgressTracker = tracker()) : AbstractStateReplacementFlow.Instigator(originalState, newNotary, progressTracker) { - override fun assembleTx(): Pair> { + override fun assembleTx(): Pair> { val state = originalState.state val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) - val participants: Iterable + val participants: Iterable if (state.encumbrance == null) { val modifiedState = TransactionState(state.data, modification) @@ -39,10 +40,7 @@ class NotaryChangeFlow( participants = resolveEncumbrances(tx) } - val myKey = serviceHub.legalIdentityKey - tx.signWith(myKey) - - val stx = tx.toSignedTransaction(false) + val stx = serviceHub.signInitialTransaction(tx) return Pair(stx, participants) } @@ -53,14 +51,14 @@ class NotaryChangeFlow( * * @return union of all added states' participants */ - private fun resolveEncumbrances(tx: TransactionBuilder): Iterable { + private fun resolveEncumbrances(tx: TransactionBuilder): Iterable { val stateRef = originalState.ref val txId = stateRef.txhash val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) ?: throw StateReplacementException("Transaction $txId not found") val outputs = issuingTx.tx.outputs - val participants = mutableSetOf() + val participants = mutableSetOf() var nextStateIndex = stateRef.index var newOutputPosition = tx.outputStates().size diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt index 58819c9c7b..32ff54484b 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt @@ -3,7 +3,10 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.StateRef import net.corda.core.contracts.Timestamp -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.crypto.keys import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow @@ -144,8 +147,7 @@ object NotaryFlow { } private fun sign(bits: ByteArray): DigitalSignature.WithKey { - val mySigningKey = serviceHub.notaryIdentityKey - return mySigningKey.sign(bits) + return serviceHub.keyManagementService.sign(bits, serviceHub.notaryIdentityKey) } private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException { diff --git a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index 6284955239..c776be45e8 100644 --- a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -2,21 +2,21 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.DealState -import net.corda.core.crypto.* +import net.corda.core.contracts.requireThat +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.expandedCompositeKeys import net.corda.core.flows.FlowLogic import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo +import net.corda.core.node.services.ServiceType import net.corda.core.seconds import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder -import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker -import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap -import java.security.KeyPair import java.security.PublicKey /** @@ -26,7 +26,7 @@ import java.security.PublicKey * * TODO: Also, the term Deal is used here where we might prefer Agreement. * - * TODO: Consider whether we can merge this with [TwoPartyTradeFlow] + * TODO: Make this flow more generic. * */ object TwoPartyDealFlow { @@ -34,151 +34,57 @@ object TwoPartyDealFlow { @CordaSerializable data class Handshake(val payload: T, val publicKey: PublicKey) - @CordaSerializable - class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List) - /** * Abstracted bilateral deal flow participant that initiates communication/handshake. - * - * There's a good chance we can push at least some of this logic down into core flow logic - * and helper methods etc. */ abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic() { companion object { - object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal") - object VERIFYING : ProgressTracker.Step("Verifying proposed transaction") - object SIGNING : ProgressTracker.Step("Signing transaction") - object NOTARY : ProgressTracker.Step("Getting notary signature") - object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party") - object RECORDING : ProgressTracker.Step("Recording completed transaction") - object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator") - - fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR) + object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.") + fun tracker() = ProgressTracker(SENDING_PROPOSAL) } abstract val payload: Any abstract val notaryNode: NodeInfo abstract val otherParty: Party - abstract val myKeyPair: KeyPair - - @Suspendable - fun getPartialTransaction(): UntrustworthyData { - progressTracker.currentStep = AWAITING_PROPOSAL + abstract val myKey: PublicKey + @Suspendable override fun call(): SignedTransaction { + progressTracker.currentStep = SENDING_PROPOSAL // Make the first message we'll send to kick off the flow. - val hello = Handshake(payload, myKeyPair.public) - val maybeSTX = sendAndReceive(otherParty, hello) + val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey) + // Wait for the FinalityFlow to finish on the other side and return the tx when it's available. + send(otherParty, hello) - return maybeSTX - } - - @Suspendable - fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData): SignedTransaction { - progressTracker.currentStep = VERIFYING - - untrustedPartialTX.unwrap { stx -> - progressTracker.nextStep() - - // Check that the tx proposed by the buyer is valid. - val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.notaryIdentity.owningKey) - logger.trace { "Received partially signed transaction: ${stx.id}" } - - checkDependencies(stx) - - // This verifies that the transaction is contract-valid, even though it is missing signatures. - wtx.toLedgerTransaction(serviceHub).verify() - - // There are all sorts of funny games a malicious secondary might play here, we should fix them: - // - // - This tx may attempt to send some assets we aren't intending to sell to the secondary, if - // we're reusing keys! So don't reuse keys! - // - This tx may include output states that impose odd conditions on the movement of the cash, - // once we implement state pairing. - // - // but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to - // express flow state machines on top of the messaging layer. - - return stx - } - } - - @Suspendable - private fun checkDependencies(stx: SignedTransaction) { - // Download and check all the transactions that this transaction depends on, but do not check this - // transaction itself. - val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet() - subFlow(ResolveTransactionsFlow(dependencyTxIDs, otherParty)) - } - - @Suspendable - override fun call(): SignedTransaction { - val stx: SignedTransaction = verifyPartialTransaction(getPartialTransaction()) - - // These two steps could be done in parallel, in theory. Our framework doesn't support that yet though. - val ourSignature = computeOurSignature(stx) - val allPartySignedTx = stx + ourSignature - val notarySignatures = getNotarySignatures(allPartySignedTx) - - val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures) - - progressTracker.currentStep = RECORDING - - serviceHub.recordTransactions(fullySigned) - - logger.trace { "Deal stored" } - - progressTracker.currentStep = COPYING_TO_REGULATOR - val regulators = serviceHub.networkMapCache.regulatorNodes - if (regulators.isNotEmpty()) { - // If there are regulators in the network, then we could copy them in on the transaction via a sub-flow - // which would simply send them the transaction. + val signTransactionFlow = object : SignTransactionFlow(otherParty) { + override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx) } - return fullySigned + subFlow(signTransactionFlow) + + val txHash = receive(otherParty).unwrap { it } + + return waitForLedgerCommit(txHash) } - @Suspendable - private fun getNotarySignatures(stx: SignedTransaction): List { - progressTracker.currentStep = NOTARY - return subFlow(NotaryFlow.Client(stx)) - } - - open fun computeOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { - progressTracker.currentStep = SIGNING - return myKeyPair.sign(partialTX.id) - } - - @Suspendable - private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey, - notarySignatures: List): SignedTransaction { - progressTracker.currentStep = SENDING_SIGS - val fullySigned = allPartySignedTx + notarySignatures - - logger.trace { "Built finished transaction, sending back to other party!" } - - send(otherParty, SignaturesFromPrimary(ourSignature, notarySignatures)) - return fullySigned - } + @Suspendable abstract fun checkProposal(stx: SignedTransaction) } - /** * Abstracted bilateral deal flow participant that is recipient of initial communication. - * - * There's a good chance we can push at least some of this logic down into core flow logic - * and helper methods etc. */ abstract class Secondary(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic() { companion object { - object RECEIVING : ProgressTracker.Step("Waiting for deal info") - object VERIFYING : ProgressTracker.Step("Verifying deal info") - object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal") - object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party") - object RECORDING : ProgressTracker.Step("Recording completed transaction") + object RECEIVING : ProgressTracker.Step("Waiting for deal info.") + object VERIFYING : ProgressTracker.Step("Verifying deal info.") + object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.") + object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.") + object RECORDING : ProgressTracker.Step("Recording completed transaction.") + object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator.") + object COPYING_TO_COUNTERPARTY : ProgressTracker.Step("Copying counterparty.") - fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING) + fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING, COPYING_TO_REGULATOR, COPYING_TO_COUNTERPARTY) } abstract val otherParty: Party @@ -188,23 +94,35 @@ object TwoPartyDealFlow { val handshake = receiveAndValidateHandshake() progressTracker.currentStep = SIGNING - val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake) - val stx = signWithOurKeys(additionalSigningPubKeys, ptx) + val (utx, additionalSigningPubKeys) = assembleSharedTX(handshake) + val ptx = signWithOurKeys(additionalSigningPubKeys, utx) - val signatures = swapSignaturesWithPrimary(stx) + logger.trace { "Signed proposed transaction." } + + progressTracker.currentStep = COLLECTING_SIGNATURES + val stx = subFlow(CollectSignaturesFlow(ptx)) logger.trace { "Got signatures from other party, verifying ... " } - val fullySigned = stx + signatures.sellerSig + signatures.notarySigs - fullySigned.verifySignatures() - - logger.trace { "Signatures received are valid. Deal transaction complete! :-)" } - progressTracker.currentStep = RECORDING - serviceHub.recordTransactions(fullySigned) + val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single() - logger.trace { "Deal transaction stored" } - return fullySigned + logger.trace { "Recorded transaction." } + + progressTracker.currentStep = COPYING_TO_REGULATOR + val regulators = serviceHub.networkMapCache.regulatorNodes + if (regulators.isNotEmpty()) { + // Copy the transaction to every regulator in the network. This is obviously completely bogus, it's + // just for demo purposes. + regulators.forEach { send(it.serviceIdentities(ServiceType.regulator).first(), ftx) } + } + + progressTracker.currentStep = COPYING_TO_COUNTERPARTY + // Send the final transaction hash back to the other party. + // We need this so we don't break the IRS demo and the SIMM Demo. + send(otherParty, ftx.id) + + return ftx } @Suspendable @@ -217,24 +135,9 @@ object TwoPartyDealFlow { return handshake.unwrap { validateHandshake(it) } } - @Suspendable - private fun swapSignaturesWithPrimary(stx: SignedTransaction): SignaturesFromPrimary { - progressTracker.currentStep = SWAPPING_SIGNATURES - logger.trace { "Sending partially signed transaction to other party" } - - // TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx. - - return sendAndReceive(otherParty, stx).unwrap { it } - } - private fun signWithOurKeys(signingPubKeys: List, ptx: TransactionBuilder): SignedTransaction { // Now sign the transaction with whatever keys we need to move the cash. - for (publicKey in signingPubKeys.expandedCompositeKeys) { - val privateKey = serviceHub.keyManagementService.toPrivate(publicKey) - ptx.signWith(KeyPair(publicKey, privateKey)) - } - - return ptx.toSignedTransaction(checkSufficientSignatures = false) + return serviceHub.signInitialTransaction(ptx, signingPubKeys) } @Suspendable protected abstract fun validateHandshake(handshake: Handshake): Handshake @@ -244,17 +147,20 @@ object TwoPartyDealFlow { @CordaSerializable data class AutoOffer(val notary: Party, val dealBeingOffered: DealState) - /** * One side of the flow for inserting a pre-agreed deal. */ open class Instigator(override val otherParty: Party, override val payload: AutoOffer, - override val myKeyPair: KeyPair, + override val myKey: PublicKey, override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() { override val notaryNode: NodeInfo get() = serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() + + @Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat { + // Add some constraints here. + } } /** @@ -281,5 +187,4 @@ object TwoPartyDealFlow { return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) } } - } diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt index 4fa07b2e44..8e7d85b240 100644 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt +++ b/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt @@ -29,7 +29,7 @@ object TxKeyFlowUtilities { */ @Suspendable fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey { - val key = flow.serviceHub.keyManagementService.freshKey().public + val key = flow.serviceHub.keyManagementService.freshKey() // TODO: Generate and sign certificate for the key, once we have signing support for composite keys // (in this case the legal identity key) flow.send(otherSide, ProvidedTransactionKey(key, null)) diff --git a/core/src/main/resources/net/corda/core/node/isolated.jar b/core/src/main/resources/net/corda/core/node/isolated.jar index 1d6808b4b0..e2db13bf3c 100644 Binary files a/core/src/main/resources/net/corda/core/node/isolated.jar and b/core/src/main/resources/net/corda/core/node/isolated.jar differ diff --git a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt index 1e5034b681..dfe175ded2 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/DummyContractV2Tests.kt @@ -1,6 +1,7 @@ package net.corda.core.contracts import net.corda.core.crypto.SecureHash +import net.corda.core.utilities.ALICE import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.ALICE_PUBKEY import org.junit.Test @@ -14,7 +15,7 @@ class DummyContractV2Tests { @Test fun `upgrade from v1`() { val contractUpgrade = DummyContractV2() - val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) + val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY) val v1Ref = StateRef(SecureHash.randomSHA256(), 0) val v1StateAndRef = StateAndRef(v1State, v1Ref) val (tx, _) = DummyContractV2().generateUpgradeFromV1(v1StateAndRef) diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt index a00380e7df..8fd3bad22a 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -2,13 +2,12 @@ package net.corda.core.contracts import net.corda.contracts.asset.Cash import net.corda.core.crypto.SecureHash -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.core.utilities.DUMMY_PUBKEY_2 +import net.corda.core.identity.AbstractParty import net.corda.testing.MEGA_CORP +import net.corda.testing.MINI_CORP import net.corda.testing.ledger import net.corda.testing.transaction import org.junit.Test -import java.security.PublicKey import java.time.Instant import java.time.temporal.ChronoUnit @@ -19,9 +18,9 @@ class TransactionEncumbranceTests { val state = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_1 + owner = MEGA_CORP ) - val stateWithNewOwner = state.copy(owner = DUMMY_PUBKEY_2) + val stateWithNewOwner = state.copy(owner = MINI_CORP) val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) @@ -40,7 +39,7 @@ class TransactionEncumbranceTests { data class State( val validFrom: Instant ) : ContractState { - override val participants: List = emptyList() + override val participants: List = emptyList() override val contract: Contract = TEST_TIMELOCK_ID } } @@ -52,7 +51,7 @@ class TransactionEncumbranceTests { input { state } output(encumbrance = 1) { stateWithNewOwner } output("5pm time-lock") { timeLock } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } verifies() } } @@ -70,7 +69,7 @@ class TransactionEncumbranceTests { input("state encumbered by 5pm time-lock") input("5pm time-lock") output { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } timestamp(FIVE_PM) verifies() } @@ -89,7 +88,7 @@ class TransactionEncumbranceTests { input("state encumbered by 5pm time-lock") input("5pm time-lock") output { state } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } timestamp(FOUR_PM) this `fails with` "the time specified in the time-lock has passed" } @@ -106,7 +105,7 @@ class TransactionEncumbranceTests { transaction { input("state encumbered by 5pm time-lock") output { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } timestamp(FIVE_PM) this `fails with` "Missing required encumbrance 1 in INPUT" } @@ -118,7 +117,7 @@ class TransactionEncumbranceTests { transaction { input { state } output(encumbrance = 0) { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } this `fails with` "Missing required encumbrance 0 in OUTPUT" } } @@ -129,7 +128,7 @@ class TransactionEncumbranceTests { input { state } output(encumbrance = 2) { stateWithNewOwner } output { timeLock } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } this `fails with` "Missing required encumbrance 2 in OUTPUT" } } @@ -146,7 +145,7 @@ class TransactionEncumbranceTests { input("state encumbered by some other state") input("5pm time-lock") output { stateWithNewOwner } - command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } + command(MEGA_CORP.owningKey) { Cash.Commands.Move() } timestamp(FIVE_PM) this `fails with` "Missing required encumbrance 1 in INPUT" } diff --git a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt index c4a777948c..05b2e39479 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -11,7 +11,6 @@ import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.* -import net.corda.testing.ALICE_PUBKEY import org.junit.Test import java.security.KeyPair import kotlin.test.assertEquals @@ -95,7 +94,7 @@ class TransactionTests { @Test fun `transactions with no inputs can have any notary`() { - val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) + val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY) val inputs = emptyList>() val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) val commands = emptyList>() @@ -120,7 +119,7 @@ class TransactionTests { @Test fun `transaction verification fails for duplicate inputs`() { - val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) + val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY) val stateRef = StateRef(SecureHash.randomSHA256(), 0) val stateAndRef = StateAndRef(baseOutState, stateRef) val inputs = listOf(stateAndRef, stateAndRef) @@ -148,7 +147,7 @@ class TransactionTests { @Test fun `general transactions cannot change notary`() { val notary: Party = DUMMY_NOTARY - val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), notary) + val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) val outState = inState.copy(notary = ALICE) val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0))) val outputs = listOf(outState) diff --git a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt index 475bb65ee7..dbbc52c5e0 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -12,9 +12,7 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_PUBKEY_1 import net.corda.core.utilities.TEST_TX_TIME -import net.corda.testing.MEGA_CORP -import net.corda.testing.MEGA_CORP_PUBKEY -import net.corda.testing.ledger +import net.corda.testing.* import org.junit.Test import java.security.PublicKey import kotlin.test.* @@ -30,20 +28,20 @@ class PartialMerkleTreeTest { output("MEGA_CORP cash") { Cash.State( amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP_PUBKEY + owner = MEGA_CORP ) } output("dummy cash 1") { Cash.State( amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = DUMMY_PUBKEY_1 + owner = MINI_CORP ) } } transaction { input("MEGA_CORP cash") - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + output("MEGA_CORP cash".output().copy(owner = MINI_CORP)) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } timestamp(TEST_TX_TIME) this.verifies() @@ -61,7 +59,7 @@ class PartialMerkleTreeTest { @Test fun `building Merkle tree - no hashes`() { - assertFailsWith { MerkleTree.Companion.getMerkleTree(emptyList()) } + assertFailsWith { MerkleTree.getMerkleTree(emptyList()) } } @Test @@ -98,7 +96,7 @@ class PartialMerkleTreeTest { fun filtering(elem: Any): Boolean { return when (elem) { is StateRef -> true - is TransactionState<*> -> elem.data.participants[0].keys == DUMMY_PUBKEY_1.keys + is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys is Command -> MEGA_CORP_PUBKEY in elem.signers is Timestamp -> true is PublicKey -> elem == MEGA_CORP_PUBKEY diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt index faedf36c5d..4612349134 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -2,7 +2,6 @@ package net.corda.core.crypto import net.corda.core.div import net.corda.testing.MEGA_CORP -import net.i2p.crypto.eddsa.EdDSAEngine import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName @@ -57,7 +56,7 @@ class X509UtilitiesTest { val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert")) val subjectDN = getTestX509Name("Server Cert") val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val serverCert = X509Utilities.createServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) + val serverCert = X509Utilities.createTlsServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert serverCert.checkValidity(Date()) // throws on verification problems @@ -107,7 +106,7 @@ class X509UtilitiesTest { val tmpKeyStore = tempFile("keystore.jks") val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test")) val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") - val edDSACert = X509Utilities.createServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54")) + val edDSACert = X509Utilities.createTlsServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54")) // Save the EdDSA private key with cert chains. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") @@ -177,14 +176,14 @@ class X509UtilitiesTest { // Load signing intermediate CA cert val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") - val caCertAndKey = caKeyStore.getCertificateAndKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") + val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") // Generate server cert and private key and populate another keystore suitable for SSL X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) // Load back server certificate val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") - val serverCertAndKey = serverKeyStore.getCertificateAndKey(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") + val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) @@ -349,4 +348,18 @@ class X509UtilitiesTest { return keyStore } + @Test + fun `Get correct private key type from Keystore`() { + val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair) + val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") + keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate)) + + val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) + val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") + + assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key + assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey) + } + } diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt new file mode 100644 index 0000000000..7e30609ca7 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -0,0 +1,192 @@ +package net.corda.core.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.contracts.Command +import net.corda.core.contracts.DummyContract +import net.corda.core.contracts.TransactionType +import net.corda.core.contracts.requireThat +import net.corda.core.getOrThrow +import net.corda.core.identity.Party +import net.corda.core.node.PluginServiceHub +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.unwrap +import net.corda.flows.CollectSignaturesFlow +import net.corda.flows.FinalityFlow +import net.corda.flows.SignTransactionFlow +import net.corda.testing.MINI_CORP_KEY +import net.corda.testing.node.MockNetwork +import org.junit.After +import org.junit.Before +import org.junit.Test +import java.util.concurrent.ExecutionException +import kotlin.test.assertFailsWith + +class CollectSignaturesFlowTests { + lateinit var mockNet: MockNetwork + lateinit var a: MockNetwork.MockNode + lateinit var b: MockNetwork.MockNode + lateinit var c: MockNetwork.MockNode + lateinit var notary: Party + + @Before + fun setup() { + mockNet = MockNetwork() + val nodes = mockNet.createSomeNodes(3) + a = nodes.partyNodes[0] + b = nodes.partyNodes[1] + c = nodes.partyNodes[2] + notary = nodes.notaryNode.info.notaryIdentity + mockNet.runNetwork() + CollectSigsTestCorDapp.registerFlows(a.services) + CollectSigsTestCorDapp.registerFlows(b.services) + CollectSigsTestCorDapp.registerFlows(c.services) + } + + @After + fun tearDown() { + mockNet.stopNodes() + } + + object CollectSigsTestCorDapp { + // Would normally be called by custom service init in a CorDapp. + fun registerFlows(pluginHub: PluginServiceHub) { + pluginHub.registerFlowInitiator(TestFlow.Initiator::class.java) { TestFlow.Responder(it) } + pluginHub.registerFlowInitiator(TestFlowTwo.Initiator::class.java) { TestFlowTwo.Responder(it) } + } + } + + // With this flow, the initiators sends an "offer" to the responder, who then initiates the collect signatures flow. + // This flow is a more simplifed version of the "TwoPartyTrade" flow and is a useful example of how both the + // "collectSignaturesFlow" and "SignTransactionFlow" can be used in practise. + object TestFlow { + @InitiatingFlow + class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + send(otherParty, state) + + val flow = object : SignTransactionFlow(otherParty) { + @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tx = stx.tx + "There should only be one output state" using (tx.outputs.size == 1) + "There should only be one output state" using (tx.inputs.isEmpty()) + val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + } + } + + val stx = subFlow(flow) + val ftx = waitForLedgerCommit(stx.id) + + return ftx + } + } + + class Responder(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val state = receive(otherParty).unwrap { it } + val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity + + val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val builder = TransactionType.General.Builder(notary = notary).withItems(state, command) + val ptx = serviceHub.signInitialTransaction(builder) + val stx = subFlow(CollectSignaturesFlow(ptx)) + val ftx = subFlow(FinalityFlow(stx)).single() + + return ftx + } + } + } + + // With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to + // override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are + // receiving off the wire. + object TestFlowTwo { + @InitiatingFlow + class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity + val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey }) + val builder = TransactionType.General.Builder(notary = notary).withItems(state, command) + val ptx = serviceHub.signInitialTransaction(builder) + val stx = subFlow(CollectSignaturesFlow(ptx)) + val ftx = subFlow(FinalityFlow(stx)).single() + + return ftx + } + } + + class Responder(val otherParty: Party) : FlowLogic() { + @Suspendable override fun call(): SignedTransaction { + val flow = object : SignTransactionFlow(otherParty) { + @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat { + val tx = stx.tx + "There should only be one output state" using (tx.outputs.size == 1) + "There should only be one output state" using (tx.inputs.isEmpty()) + val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState + "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337) + } + } + + val stx = subFlow(flow) + + return waitForLedgerCommit(stx.id) + } + } + } + + + @Test + fun `successfully collects two signatures`() { + val magicNumber = 1337 + val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity) + val state = DummyContract.MultiOwnerState(magicNumber, parties) + val flow = a.services.startFlow(TestFlowTwo.Initiator(state, b.info.legalIdentity)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + result.verifySignatures() + println(result.tx) + println(result.sigs) + } + + @Test + fun `no need to collect any signatures`() { + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) + val ptx = a.services.signInitialTransaction(onePartyDummyContract) + val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + result.verifySignatures() + println(result.tx) + println(result.sigs) + } + + @Test + fun `fails when not signed by initiator`() { + val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1)) + val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false) + val flow = a.services.startFlow(CollectSignaturesFlow(ptx)) + mockNet.runNetwork() + assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { + flow.resultFuture.get() + } + } + + @Test + fun `passes with multiple initial signatures`() { + val twoPartyDummyContract = DummyContract.generateInitial(1337, notary, + a.info.legalIdentity.ref(1), + b.info.legalIdentity.ref(2), + b.info.legalIdentity.ref(3)) + val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) + val signedByBoth = b.services.addSignature(signedByA) + val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth)) + mockNet.runNetwork() + val result = flow.resultFuture.getOrThrow() + println(result.tx) + println(result.sigs) + } +} + diff --git a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt index 738802dbf2..4e71b4d222 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -1,14 +1,17 @@ package net.corda.core.flows +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.node.services.unconsumedStates import net.corda.core.serialization.OpaqueBytes +import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Emoji import net.corda.flows.CashIssueFlow import net.corda.flows.ContractUpgradeFlow @@ -25,9 +28,7 @@ import net.corda.testing.startRpcClient import org.junit.After import org.junit.Before import org.junit.Test -import java.security.PublicKey import java.util.* -import java.util.concurrent.ExecutionException import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -57,9 +58,8 @@ class ContractUpgradeFlowTest { fun `2 parties contract upgrade`() { // Create dummy contract. val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1)) - val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey) - .signWith(b.services.legalIdentityKey) - .toSignedTransaction() + val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) + val stx = b.services.addSignature(signedByA) a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity))) mockNet.runNetwork() @@ -69,10 +69,10 @@ class ContractUpgradeFlowTest { requireNotNull(atx) requireNotNull(btx) - // The request is expected to be rejected because party B haven't authorise the upgrade yet. + // The request is expected to be rejected because party B hasn't authorised the upgrade yet. val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() - assertFailsWith(ExecutionException::class) { rejectedFuture.get() } + assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade. b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) @@ -81,7 +81,7 @@ class ContractUpgradeFlowTest { val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture mockNet.runNetwork() - val result = resultFuture.get() + val result = resultFuture.getOrThrow() fun check(node: MockNetwork.MockNode) { val nodeStx = node.database.transaction { @@ -108,7 +108,7 @@ class ContractUpgradeFlowTest { rpcAddress = startRpcServer( rpcUser = user, ops = CordaRPCOpsImpl(node.services, node.smm, node.database) - ).get().hostAndPort, + ).get().broker.hostAndPort!!, username = user.username, password = user.password ).get() @@ -119,17 +119,16 @@ class ContractUpgradeFlowTest { rpcDriver { // Create dummy contract. val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1)) - val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey) - .signWith(b.services.legalIdentityKey) - .toSignedTransaction() + val signedByA = a.services.signInitialTransaction(twoPartyDummyContract) + val stx = b.services.addSignature(signedByA) val user = rpcTestUser.copy(permissions = setOf( - startFlowPermission(), + startFlowPermission(), startFlowPermission>() )) val rpcA = startProxy(a, user) val rpcB = startProxy(b, user) - val handle = rpcA.startFlow(::FinalityFlow, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) + val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) mockNet.runNetwork() handle.returnValue.getOrThrow() @@ -143,7 +142,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - assertFailsWith(ExecutionException::class) { rejectedFuture.get() } + assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() } // Party B authorise the contract state upgrade. rpcB.authoriseContractUpgrade(btx!!.tx.outRef(0), DummyContractV2::class.java) @@ -154,7 +153,7 @@ class ContractUpgradeFlowTest { DummyContractV2::class.java).returnValue mockNet.runNetwork() - val result = resultFuture.get() + val result = resultFuture.getOrThrow() // Check results. listOf(a, b).forEach { val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } @@ -186,21 +185,21 @@ class ContractUpgradeFlowTest { val firstState = a.database.transaction { a.vault.unconsumedStates().single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") - assertEquals(listOf(a.info.legalIdentity.owningKey), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") + assertEquals>(listOf(a.info.legalIdentity), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") } class CashV2 : UpgradedContract { override val legacyContract = Cash::class.java - data class State(override val amount: Amount>, val owners: List) : FungibleAsset { - override val owner: PublicKey = owners.first() - override val exitKeys = (owners + amount.token.issuer.party.owningKey).toSet() + data class State(override val amount: Amount>, val owners: List) : FungibleAsset { + override val owner: AbstractParty = owners.first() + override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet() override val contract = CashV2() override val participants = owners - override fun move(newAmount: Amount>, newOwner: PublicKey) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) + override fun move(newAmount: Amount>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)" - override fun withNewOwner(newOwner: PublicKey) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner))) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner))) } override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner)) @@ -210,4 +209,11 @@ class ContractUpgradeFlowTest { // Dummy Cash contract for testing. override val legalContractReference = SecureHash.sha256("") } + + @StartableByRPC + class FinalityInvoker(val transaction: SignedTransaction, + val extraRecipients: Set) : FlowLogic>() { + @Suspendable + override fun call(): List = subFlow(FinalityFlow(transaction, extraRecipients)) + } } diff --git a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt index 71eb351cbc..a48e72944f 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -12,7 +12,7 @@ import net.corda.flows.ResolveTransactionsFlow import net.corda.node.utilities.transaction import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY -import net.corda.testing.MINI_CORP_PUBKEY +import net.corda.testing.MINI_CORP import net.corda.testing.node.MockNetwork import org.junit.After import org.junit.Before @@ -94,7 +94,7 @@ class ResolveTransactionsFlowTest { val count = 50 var cursor = stx2 repeat(count) { - val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP_PUBKEY) + val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP) .addSignatureUnchecked(NullSignature) .toSignedTransaction(false) a.database.transaction { @@ -113,13 +113,13 @@ class ResolveTransactionsFlowTest { fun `triangle of transactions resolves fine`() { val stx1 = makeTransactions().first - val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP_PUBKEY).run { + val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run { signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) toSignedTransaction() } - val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP_PUBKEY).run { + val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run { signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) toSignedTransaction() @@ -173,7 +173,7 @@ class ResolveTransactionsFlowTest { it.signWith(DUMMY_NOTARY_KEY) it.toSignedTransaction(false) } - val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP_PUBKEY).let { + val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let { it.signWith(MEGA_CORP_KEY) it.signWith(DUMMY_NOTARY_KEY) it.toSignedTransaction() diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt index 88084bcb9e..662a807269 100644 --- a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt @@ -4,7 +4,6 @@ import net.corda.core.identity.Party import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.testing.MOCK_IDENTITY_SERVICE import net.corda.testing.node.MockNetwork import org.junit.Before import org.junit.Test @@ -17,7 +16,6 @@ class TxKeyFlowUtilitiesTests { @Before fun before() { net = MockNetwork(false) - net.identities += MOCK_IDENTITY_SERVICE.identities } @Test diff --git a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt index 33db2d08e2..127103ae5f 100644 --- a/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/AttachmentClassLoaderTests.kt @@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.mock import com.nhaarman.mockito_kotlin.whenever import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.StorageService @@ -53,7 +54,7 @@ class AttachmentClassLoaderTests { class AttachmentDummyContract : Contract { data class State(val magicNumber: Int = 0) : ContractState { override val contract = ATTACHMENT_TEST_PROGRAM_ID - override val participants: List + override val participants: List get() = listOf() } diff --git a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt index a5e5dbd951..9b15ca200e 100644 --- a/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt +++ b/core/src/test/kotlin/net/corda/core/node/VaultUpdateTests.kt @@ -2,10 +2,10 @@ package net.corda.core.node import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.utilities.DUMMY_NOTARY import org.junit.Test -import java.security.PublicKey import kotlin.test.assertEquals @@ -20,7 +20,7 @@ class VaultUpdateTests { } private class DummyState : ContractState { - override val participants: List + override val participants: List get() = emptyList() override val contract = VaultUpdateTests.DummyContract } diff --git a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt index 5d30f4b17d..4309ba3521 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -3,8 +3,11 @@ package net.corda.core.serialization import com.esotericsoftware.kryo.Kryo import com.google.common.primitives.Ints import net.corda.core.crypto.* +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB import net.corda.node.services.messaging.Ack import net.corda.node.services.persistence.NodeAttachmentService +import net.corda.testing.BOB_PUBKEY import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Before @@ -12,6 +15,8 @@ import org.junit.Test import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream import java.io.InputStream +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -136,6 +141,24 @@ class KryoTests { assertEquals(-1, readRubbishStream.read()) } + @Test + fun `serialize - deserialize X509Certififcate`() { + val expected = X509Utilities.createSelfSignedCACert(ALICE.name).certificate + val serialized = expected.serialize(kryo).bytes + val actual: X509Certificate = serialized.deserialize(kryo) + assertEquals(expected, actual) + } + + @Test + fun `serialize - deserialize X509CertPath`() { + val rootCA = X509Utilities.createSelfSignedCACert(ALICE.name) + val certificate = X509Utilities.createTlsServerCert(BOB.name, BOB_PUBKEY, rootCA, emptyList(), emptyList()) + val expected = X509Utilities.createCertificatePath(rootCA, certificate, false).certPath + val serialized = expected.serialize(kryo).bytes + val actual: CertPath = serialized.deserialize(kryo) + assertEquals(expected, actual) + } + @CordaSerializable private data class Person(val name: String, val birthday: Instant?) diff --git a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt index 8108ac17f3..e0e1cd17df 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -2,14 +2,19 @@ package net.corda.core.serialization import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder -import net.corda.core.utilities.* +import net.corda.core.utilities.DUMMY_KEY_2 +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.core.utilities.TEST_TX_TIME +import net.corda.testing.MEGA_CORP +import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.MINI_CORP import net.corda.testing.generateStateRef import org.junit.Before import org.junit.Test -import java.security.PublicKey import java.security.SignatureException import java.util.* import kotlin.test.assertEquals @@ -27,12 +32,12 @@ class TransactionSerializationTests { data class State( val deposit: PartyAndReference, val amount: Amount, - override val owner: PublicKey) : OwnableState { + override val owner: AbstractParty) : OwnableState { override val contract: Contract = TEST_PROGRAM_ID - override val participants: List + override val participants: List get() = listOf(owner) - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) } interface Commands : CommandData { @@ -44,9 +49,9 @@ class TransactionSerializationTests { // It refers to a fake TX/state that we don't bother creating here. val depositRef = MINI_CORP.ref(1) val fakeStateRef = generateStateRef() - val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef) - val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY) - val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, DUMMY_KEY_1.public), DUMMY_NOTARY) + val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), DUMMY_NOTARY), fakeStateRef) + val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), DUMMY_NOTARY) + val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), DUMMY_NOTARY) lateinit var tx: TransactionBuilder @@ -54,14 +59,14 @@ class TransactionSerializationTests { @Before fun setup() { tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems( - inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(DUMMY_KEY_1.public)) + inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(MEGA_CORP.owningKey)) ) } @Test fun signWireTX() { tx.signWith(DUMMY_NOTARY_KEY) - tx.signWith(DUMMY_KEY_1) + tx.signWith(MEGA_CORP_KEY) val signedTX = tx.toSignedTransaction() // Now check that the signature we just made verifies. @@ -81,7 +86,7 @@ class TransactionSerializationTests { tx.toSignedTransaction() } - tx.signWith(DUMMY_KEY_1) + tx.signWith(MEGA_CORP_KEY) tx.signWith(DUMMY_NOTARY_KEY) val signedTX = tx.toSignedTransaction() @@ -104,7 +109,7 @@ class TransactionSerializationTests { @Test fun timestamp() { tx.setTime(TEST_TX_TIME, 30.seconds) - tx.signWith(DUMMY_KEY_1) + tx.signWith(MEGA_CORP_KEY) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint) diff --git a/core/src/test/kotlin/net/corda/core/testing/Generators.kt b/core/src/test/kotlin/net/corda/core/testing/Generators.kt index dabc585926..436ef4125e 100644 --- a/core/src/test/kotlin/net/corda/core/testing/Generators.kt +++ b/core/src/test/kotlin/net/corda/core/testing/Generators.kt @@ -71,7 +71,7 @@ class SecureHashGenerator : Generator(SecureHash::class.java) { class StateRefGenerator : Generator(StateRef::class.java) { override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef { - return StateRef(SecureHash.Companion.sha256(random.nextBytes(16)), random.nextInt(0, 10)) + return StateRef(SecureHash.sha256(random.nextBytes(16)), random.nextInt(0, 10)) } } diff --git a/docs/make-docsite.sh b/docs/make-docsite.sh index 36865fdcd1..8c2488ca04 100755 --- a/docs/make-docsite.sh +++ b/docs/make-docsite.sh @@ -11,4 +11,4 @@ else source virtualenv/Scripts/activate fi -make html \ No newline at end of file +make html diff --git a/docs/source/_static/corda-introductory-whitepaper-zhs.pdf b/docs/source/_static/corda-introductory-whitepaper-zhs.pdf new file mode 100644 index 0000000000..2c02bec5c5 Binary files /dev/null and b/docs/source/_static/corda-introductory-whitepaper-zhs.pdf differ diff --git a/docs/source/_static/corda-introductory-whitepaper-zht.pdf b/docs/source/_static/corda-introductory-whitepaper-zht.pdf new file mode 100644 index 0000000000..1038b6b9b4 Binary files /dev/null and b/docs/source/_static/corda-introductory-whitepaper-zht.pdf differ diff --git a/docs/source/azure-vm.rst b/docs/source/azure-vm.rst index 88c2c86309..50f94d072e 100644 --- a/docs/source/azure-vm.rst +++ b/docs/source/azure-vm.rst @@ -1,175 +1,205 @@ -Working with the Corda Demo on Azure Marketplace -================================================ +Building a Corda Network on Azure Marketplace +============================================= -Corda ships with a VM image which can be used to deploy a pre-configured virtual machine on the `Microsoft Azure Marketplace `_ +To help you design, build and test applications on Corda, called CorDapps, a Corda network can be deployed on the `Microsoft Azure Marketplace `_ - -This Corda Demo VM is an easy option for running the demos; it is *NOT* a development environment. When you are ready to get developing on Corda and start making contributions to the project please clone the `GitHub Repos `_ instead. +This Corda network offering builds a pre-configured network of Corda nodes as Ubuntu virtual machines (VM). The network comprises of a Network Map Service node, a Notary node and up to nine Corda nodes using a version of Corda of your choosing. The following guide will also show you how to load a simple Yo! CorDapp which demonstrates the basic principles of Corda. When you are ready to go further with developing on Corda and start making contributions to the project head over to the `Corda.net `_. Pre-requisites -------------- -* Ensure you have a registered Microsoft Azure account and are logged on to the Azure portal. +* Ensure you have a registered Microsoft Azure account which can create virtual machines under your subscription(s) and you are logged on to the Azure portal (portal.azure.com) * It is recommended you generate a private-public SSH key pair (see `here `_) -Deploying the VM ----------------- +Deploying the Corda Network +--------------------------- + +Browse to portal.azure.com, login and search the Azure Marketplace for Corda and select 'Corda Single Ledger Network'. -Search the Azure Marketplace for Corda. Click the 'Create' button. STEP 1: Basics -* **Name**: Choose an appropriate descriptive name for the VM -* **VM Disk Type**: Select 'SSD' -* **Username**: Your preferred user name for the administrator account when accessing via SSH -* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box below. Alternatively select 'Password' to use a password of your choice to administer the VM +Define the basic parameters which will be used to pre-configure your Corda nodes. -* **Subscription**: Select your subscription name -* **Resource group**: Select 'Use existing'. From the drop-down menu, select your account group +* **Resource prefix**: Choose an appropriate descriptive name for your Corda nodes. This name will prefix the node hostnames +* **VM user name**: This is the user login name on the Ubuntu VMs. Leave it as azureuser or define your own +* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box. Alternatively select 'Password' to use a password of your choice to administer the VM +* **Restrict access by IP address**: Leave this as 'No' to allow access from any internet host, or provide an IP address or a range of IP addresses to limit access +* **Subscription**: Select which of your Azure subscriptions you want to use +* **Resource group**: Choose to 'Create new' and provide a useful name of your choice * **Location**: Select the geographical location physically closest to you -.. image:: resources/azure_vm_10_00_1.png +.. image:: resources/azure_multi_node_step1.png :width: 300px Click 'OK' -STEP 2: Size +STEP 2: Network Size and Performance -A range of available hardware configurations will be presented, along with estimated costs. For the purposes of running the demos, a configuration of 2 cores and at least 14GB is recommended +Define the number of Corda nodes in your network and the size of VM. -.. image:: resources/azure_vm_10_05_1.png +* **Number of Network Map nodes**: There can only be one Network Map node in this network. Leave as '1' +* **Number of Notary nodes**: There can only be one Notary node in this network. Leave as '1' +* **Number of participant nodes**: This is the number of Corda nodes in your network. At least 2 nodes in your network is recommended (so you can send transactions between them). You can specific 1 participant node and use the Notary node as a second node. There is an upper limit of 9 +* **Storage performance**: Leave as 'Standard' +* **Virtual machine size**: The size of the VM is automatically adjusted to suit the number of participant nodes selected. It is recommended to use the suggested values + +.. image:: resources/azure_multi_node_step2.png :width: 300px -Choose the required configuration and click 'Select'. +Click 'OK' -STEP 3: Settings +STEP 3: Corda Specific Options -Adjust any configuration settings required. For the purposes of running the Corda demos, all settings may be left as default. +Define the version of Corda you want on your nodes and the type of notary. -.. image:: resources/azure_vm_10_16_1.png +* **Corda version (as seen in Maven Central)**: Select the version of Corda you want your nodes to use from the drop down list. The version numbers can be seen in `Maven Central `_, for example 0.11.0 +* **Notary type**: Select either 'Non Validating' (notary only checks whether a state has been previously used and marked as historic) or 'Validating' (notary performs transaction verification by seeing input and output states, attachments and other transaction information). More information on notaries can be found `here `_ + +.. image:: resources/azure_multi_node_step3.png :width: 300px + +Click 'OK' STEP 4: Summary -The banner at the top of the dialog should read 'Validation passed' otherwise go back and adjust settings where needed. +A summary of your selections is shown. -.. image:: resources/azure_vm_10_19.png +.. image:: resources/azure_multi_node_step4.png :width: 300px -Click 'OK' to proceed. +Click 'OK' for your selection to be validated. If everything is ok you will see the message 'Validation passed' + +Click 'OK' STEP 5: Buy -Click 'Purchase' to complete the configuration and start the VM deployment. +Review the Azure Terms of Use and Privacy Policy and click 'Purchase' to buy the Azure VMs which will host your Corda nodes. -The VM will begin the deployment process, which typically takes 4-5 minutes to complete. To see progress, click on the "Deploying" icon displayed. +The deployment process will start and typically takes 8-10 minutes to complete. -.. image:: resources/azure_vm_10_20.png +Once deployed click 'Resources Groups', select the resource group you defined in Step 1 above and click 'Overview' to see the virtual machine details. The names of your VMs will be pre-fixed with the resource prefix value you defined in Step 1 above. + +The Newtork Map Service node is suffixed nm0. The Notary node is suffixed not0. Your Corda participant nodes are suffixed node0, node1, node2 etc. Note down the **Public IP address** for your Corda nodes. You will need these to connect to UI screens via your web browser: + +.. image:: resources/azure_ip.png :width: 300px -Once deployed, click 'Overview' to see the virtual machine details. Note down the **Public IP address**. You will need this to connect to the demo screens via your web browser: +Using the Yo! CorDapp +--------------------- +Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages to other Corda nodes on the network. A Yo! message is a very simple transaction. The Yo! CorDapp demonstrates: -.. image:: resources/azure_vm_10_26.png - :width: 300px +- how transactions are only sent between Corda nodes which they are intended for and are not shared across the entire network by using the network map +- uses a pre-defined flow to orchestrate the ledger update automatically +- the contract imposes rules on the ledger updates -Viewing the SIMM Valuation demo -------------------------------- -The SIMM Valuation demo creates three nodes, representing three parties in the example workflow (Bank A, Bank B, Bank C). Each node listens on a different port - those used by the demo are: +* **Loading the Yo! CorDapp onto your nodes** -**SIMM Valuation Demo ports:** **12005 (node A for Bank A)**, **12007 (node B for Bank B)**, **12009 (node C for Bank C)** +The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory. -Open three browser tabs and direct each one to +Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command: + +For Corda nodes running release M10 .. sourcecode:: shell - http://(public IP address):(port)/web/simmvaluationdemo + cd /opt/corda/plugins + wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar -specifying each of the three ports above in different windows, e.g. +For Corda nodes running release M11 .. sourcecode:: shell - http://51.140.41.48/12005/web/simmvaluationdemo + cd /opt/corda/plugins + wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar -You will be able to view the basic web interface identifying the different banks. +Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal: -Now let's take a look at a transaction between Bank A and B which is not visible to Bank C. This illustrates the restricted data sharing feature of Corda, i.e. data is shared on a need-to-know basis. Nodes provide the dependency graph of a transaction they are sending to another node on demand, but there is no global broadcast of all transactions. +.. sourcecode:: shell -1. In the browser tab for Bank A (the top right hand corner shows which bank you are administering) click 'Create New Trade' from the top navigation bar -2. Select to trade with Bank B -3. Select 'EUR Fixed 1y EURIBOR 3m' from the drop down -4. Click 'Submit' to create the trade -5. In the browser tab for Bank B click 'View Portfolio' from the top navigation bar to see this new trade -6. In the browser tab for Bank C click 'View Portfolio' from the top navigation bar and you will not be able to see the trade, as expected + sudo systemctl restart corda + sudo systemctl restart corda-webserver -.. image:: resources/azure_vm_10_51.png +Repeat these steps on other Corda nodes on your network which you want to send or receive Yo messages. + +* **Verify the Yo! CorDapp is running** + +Open a browser tab and browse to the following URL: + +.. sourcecode:: shell + + http://(public IP address):(port)/web/yo + +where (public IP address) is the public IP address of one of your Corda nodes on the Azure Corda network and (port) is the web server port number for your Corda node, 10004 by default + +You will now see the Yo! CordDapp web interface: + +.. image:: resources/Yo_web_ui.png :width: 300px -.. note:: There is a known issue whereby some users may see a 400 error when navigating the SIMM Valuation demo. If you encounter this error, simply navigate back to the root page (http://*(public IP address)*:*(port)*/*web*/*simmvaluationdemo*) in the browser before continuing. +* **Sending a Yo message via the web interface** -Viewing the IRS demo --------------------- -The IRS demo creates three nodes: Bank A, Bank B and a node that runs a notary, a network map and an interest rates oracle together. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the time on a simulated clock passes. Each bank node listens on a different port - those used by the demo are: - -**IRS demo ports:** **11005 (node A for Bank A)**, **11007 (node B for Bank B)** - -Open two browser tabs and direct one to each of the following: +In the browser window type the following URL to send a Yo message to a target node on your Corda network: .. sourcecode:: shell - http://localhost:11005/web/irsdemo - http://localhost:11007/web/irsdemo + http://(public IP address):(port)/api/yo/yo?target=(legalname of target node) -You will be able to see the nodes' view of the ledger. +where (public IP address) is the public IP address of one of your Corda nodes on the Azure Corda network and (port) is the web server port number for your Corda node, 10004 by default and (legalname of target node) is the Legal Name for the target node as defined in the node.conf file, for example: -.. image:: resources/azure_vm_10_52.png +.. sourcecode:: shell + + http://40.69.40.42:10004/api/yo/yo?target=Corda 0.10.1 Node 1 in tstyo2 + +An easy way to see the Legal Names of Corda nodes on the network is to use the peers screen: + +.. sourcecode:: shell + + http://(public IP address):(port)/api/yo/peers + +.. image:: resources/yo_peers2.png :width: 300px -Now let's take a look at how the interest rates oracle provides interest rates for a deal with a semi-annual payment frequency, and how the two counterparties to the trade see the same deal information on their own nodes, i.e. you see what I see. +* **Viewing Yo messages** -1. In the browser tab for Bank A click 'Create Deal' from the top navigation bar -2. Modify the terms of the IRS deal, or leave as default -3. Click 'Submit' to create the deal -4. In the browser tab for Bank A click 'Recent Deals' from the top navigation bar to view the deal -5. In the browser tab for Bank B click 'Recent Deals' from the top navigation bar to view the deal. Compare the economic details to those shown in the Bank A tab +To see Yo! messages sent to a particular node open a browser window and browse to the following URL: -.. image:: resources/azure_vm_10_54.png +.. sourcecode:: shell + + http://(public IP address):(port)/api/yo/yos + +.. image:: resources/azure_yos.png :width: 300px - -Viewing logs (advanced users) ------------------------------ +Viewing logs +------------ Users may wish to view the raw logs generated by each node, which contain more information about the operations performed by each node. You can access these using an SSH client of your choice (e.g. Putty) and logging into the virtual machine using the public IP address. -Once logged in, navigate to +Once logged in, navigate to the following directory for Corda logs (node-xxxxxx): .. sourcecode:: shell - /opt/simm-nodes/ + /opt/corda/logs -for the SIMM Valuation demo logs and +And navigate to the following directory for system logs (syslog): .. sourcecode:: shell - /opt/irs-nodes/ - -for the IRS demo logs. -There are separate sub-directories for each of the three nodes (*nodea*, *nodeb*, *nodec*), each containing a */logs* sub-directory. - -The name of the log file will follow the name given to the service it reflects, e.g. *node-clint-vm-test.log*. - -.. image:: resources/azure_vm_10_47.png - :width: 300px + /var/log You can open log files with any text editor. .. image:: resources/azure_vm_10_49.png :width: 300px + +.. image:: resources/azure_syslog.png + :width: 300px Next Steps ---------- -Now you have taken a look at two Corda demos do go and visit the `dedicated Corda website `_ +Now you have built a Corda network and used a basic Corda CorDapp do go and visit the `dedicated Corda website `_ -Or to get straight into the Corda open source codebase, head over to the `Github Corda repo `_ +Or to join the growing Corda community and get straight into the Corda open source codebase, head over to the `Github Corda repo `_ diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 967834c1b1..59d866917b 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -8,8 +8,11 @@ UNRELEASED ---------- * API changes: - * Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with - ``InitiatingFlow``. + * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with + ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``. + + * Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now + required to be annotated with ``@InitiatingFlow``. * ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an @@ -29,25 +32,55 @@ UNRELEASED * ``FlowLogic.getCounterpartyMarker`` is no longer used and been deprecated for removal. If you were using this to manage multiple independent message streams with the same party in the same flow then use sub-flows instead. - * There are major changes to the ``Party`` class as part of confidential identities: * ``Party`` has moved to the ``net.corda.core.identity`` package; there is a deprecated class in its place for backwards compatibility, but it will be removed in a future release and developers should move to the new class as soon as possible. - * There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. A new class - ``AnonymousParty`` has been added, which is intended to be used in place of ``Party`` or ``PublicKey`` in contract - state objects. The exception to this is where the party in a contract state is intended to be well known, such as - issuer of a ``Cash`` state. + * There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. This now replaces + use of ``Party`` and ``PublicKey`` in state objects, and allows use of full or anonymised parties depending on + use-case. * Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the name. As a result all node legal names must now be structured as X.500 distinguished names. + * There are major changes to transaction signing in flows: + + * You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most + of the details of this for you. They may get more complex in future as signing becomes a more featureful + operation. + * ``ServiceHub.legalIdentityKey`` no longer returns a ``KeyPair``, it instead returns just the ``PublicKey`` portion of this pair. + The ``ServiceHub.notaryIdentityKey`` has changed similarly. The goal of this change is to keep private keys + encapsulated and away from most flow code/Java code, so that the private key material can be stored in HSMs + and other key management devices. + * The ``KeyManagementService`` now provides no mechanism to request the node's ``PrivateKey`` objects directly. + Instead signature creation occurs in the ``KeyManagementService.sign``, with the ``PublicKey`` used to indicate + which of the node's multiple keys to use. This lookup also works for ``CompositeKey`` scenarios + and the service will search for a leaf key hosted on the node. + * The ``KeyManagementService.freshKey`` method now returns only the ``PublicKey`` portion of the newly generated ``KeyPair`` + with the ``PrivateKey`` kept internally to the service. + * Flows which used to acquire a node's ``KeyPair``, typically via ``ServiceHub.legalIdentityKey``, + should instead use the helper methods on ``ServiceHub``. In particular to freeze a ``TransactionBuilder`` and + generate an initial partially signed ``SignedTransaction`` the flow should use ``ServiceHub.signInitialTransaction``. + Flows generating additional party signatures should use ``ServiceHub.createSignature``. Each of these methods is + provided with two signatures. One version that signs with the default node key, the other which allows key selection + by passing in the ``PublicKey`` partner of the desired signing key. + * The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but + should only be used as part of unit testing. + * The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version number, defaulting to 1 if it's specified. The flow version is included in the flow session request and the counterparty will only respond and start their own flow if the version number matches to the one they've registered with. At some point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards compatibility of CorDapp flows. +Milestone 11.1 +-------------- + +* Fix serialisation error when starting a flow. +* Automatically whitelist subclasses of `InputStream` when serialising. +* Fix exception in DemoBench on Windows when loading CorDapps into the Node Explorer. +* Detect when localhost resolution is broken on MacOSX, and provide instructions on how to fix it. + Milestone 11.0 -------------- diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 1b1a44803e..82def31fed 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -6,14 +6,15 @@ compatible language the easiest way to do so is using the client library. The li node using a message queue protocol and then provides a simple RPC interface to interact with it. You make calls on a Java object as normal, and the marshalling back and forth is handled for you. -The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``proxy`` method that -returns an implementation of the `CordaRPCOps`_ interface. A timeout parameter can be specified, and observables that -are returned by RPCs can be subscribed to in order to receive an ongoing stream of updates from the node. More -detail on how to use this is provided in the docs for the proxy method. +The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``start`` method that +returns a `CordaRPCConnection`_, holding an implementation of the `CordaRPCOps`_ that may be accessed with ``proxy`` +in Kotlin and ``getProxy()`` in Java. Observables that are returned by RPCs can be subscribed to in order to receive +an ongoing stream of updates from the node. More detail on how to use this is provided in the docs for the proxy method. -.. warning:: The returned object is somewhat expensive to create and consumes a small amount of server side - resources. When you're done with it, cast it to ``Closeable`` or ``AutoCloseable`` and close it. Don't create - one for every call you make - create a proxy and reuse it. +.. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of + server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use`` + method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create + a new proxy for every call you make - reuse an existing one. For a brief tutorial on how one can use the RPC API see :doc:`tutorial-clientrpc-api`. @@ -34,25 +35,21 @@ The returned observable may even emit object graphs with even more observables i would expect. This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you -download them. Therefore RPCs that use this feature are marked with the ``@RPCReturnsObservables`` annotation, and -you are expected to subscribe to all the observables returned. If you don't want an observable then subscribe -then unsubscribe immediately to clear the buffers and indicate that you aren't interested. If your app quits then -server side resources will be freed automatically. +download them. Note that the server side observation buffer is bounded, once it fills up the client is considered +slow and kicked. You are expected to subscribe to all the observables returned, otherwise client-side memory starts +filling up as observations come in. If you don't want an observable then subscribe then unsubscribe immediately to +clear the client-side buffers and to stop the server from streaming. If your app quits then server side resources +will be freed automatically. -When all the observables returned by an RPC are unsubscribed on the client side, that unsubscription propagates -through to the server where the corresponding server-side observables are also unsubscribed. - -.. warning:: If you leak an observable or proxy on the client side and it gets garbage collected, you will get - a warning printed to the logs and the proxy will be closed for you. But don't rely on this, as garbage - collection is non-deterministic. +.. warning:: If you leak an observable on the client side and it gets garbage collected, you will get a warning + printed to the logs and the observable will be unsubscribed for you. But don't rely on this, as garbage collection + is non-deterministic. Futures ------- A method can also return a ``ListenableFuture`` in its object graph and it will be treated in a similar manner to -observables, including needing to mark the RPC with the ``@RPCReturnsObservables`` annotation. Unlike for an observable, -once the single value (or an exception) has been received all server-side resources will be released automatically. Calling -the ``cancel`` method on the future will unsubscribe it from any future value and release any resources. +observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release any resources. Versioning ---------- @@ -66,13 +63,9 @@ of, an ``UnsupportedOperationException`` is thrown. If you want to know the vers Thread safety ------------- -A proxy is thread safe, blocking, and will only allow a single RPC to be in flight at once. Any observables that -are returned and you subscribe to will have objects emitted on a background thread. Observables returned as part -of one RPC and observables returned from another may have their callbacks invoked in parallel, but observables -returned as part of the same specific RPC invocation are processed serially and will not be invoked in parallel. - -If you want to make multiple calls to the server in parallel you can do that by creating multiple proxies, but -be aware that the server itself may *not* process your work in parallel even if you make your requests that way. +A proxy is thread safe, blocking, and allows multiple RPCs to be in flight at once. Any observables that are returned and +you subscribe to will have objects emitted in order on a background thread pool. Each Observable stream is tied to a single +thread, however note that two separate Observables may invoke their respective callbacks on different threads. Error handling -------------- @@ -85,8 +78,7 @@ side as if it was thrown from inside the called RPC method. These exceptions can Wire protocol ------------- -The client RPC wire protocol is not currently documented. To use it you must use the client library provided. -This is likely to change in a future release. +The client RPC wire protocol is defined and documented in ``net/corda/client/rpc/RPCApi.kt``. Whitelisting classes with the Corda node ---------------------------------------- @@ -98,5 +90,6 @@ with the annotation ``@CordaSerializable``. See :doc:`creating-a-cordapp` or :d .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. -.. _CordaRPCClient: api/kotlin/corda/net.corda.client.rpc/-corda-r-p-c-client/index.html -.. _CordaRPCOps: api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html +.. _CordaRPCClient: api/javadoc/net/corda/client/rpc/CordaRPCClient.html +.. _CordaRPCOps: api/javadoc/net/corda/core/messaging/CordaRPCOps.html +.. _CordaRPCConnection: api/javadoc/net/corda/client/rpc/CordaRPCConnection.html diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index 290e687bc4..39feba07ee 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -81,7 +81,8 @@ path to the node's base directory. .. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However, note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed - here must be externally accessible when running nodes across a cluster of machines. + here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable, + the node will try to auto-discover its public one. :rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC. diff --git a/docs/source/corda-plugins.rst b/docs/source/corda-plugins.rst index 033b79a771..75a893449a 100644 --- a/docs/source/corda-plugins.rst +++ b/docs/source/corda-plugins.rst @@ -45,19 +45,7 @@ extensions to be created, or registered at startup. In particular: jars. These static serving directories will not be available if the bundled web server is not started. - c. The ``requiredFlows`` property is used to declare new protocols in - the plugin jar. Specifically the property must return a map with a key - naming each exposed top level flow class and a value which is a set - naming every parameter class that will be passed to the flow's - constructor. Standard ``java.lang.*`` and ``kotlin.*`` types do not need - to be included, but all other parameter types, or concrete interface - implementations need declaring. Declaring a specific flow in this map - white lists it for activation by the ``FlowLogicRefFactory``. White - listing is not strictly required for ``subFlows`` used internally, but - is required for any top level flow, or a flow which is invoked through - the scheduler. - - d. The ``servicePlugins`` property returns a list of classes which will + c. The ``servicePlugins`` property returns a list of classes which will be instantiated once during the ``AbstractNode.start`` call. These classes must provide a single argument constructor which will receive a ``PluginServiceHub`` reference. They must also extend the abstract class @@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular: functions inside the node, for instance to initiate workflows when certain conditions are met. - e. The ``customizeSerialization`` function allows classes to be whitelisted + d. The ``customizeSerialization`` function allows classes to be whitelisted for object serialisation, over and above those tagged with the ``@CordaSerializable`` annotation. In general the annotation should be preferred. For instance new state types will need to be explicitly registered. This will be called at diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/creating-a-cordapp.rst index dcff06d0de..c4c808aeb5 100644 --- a/docs/source/creating-a-cordapp.rst +++ b/docs/source/creating-a-cordapp.rst @@ -12,11 +12,10 @@ App plugins To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains specific details of the implementation, but you can extend the server in the following ways: -1. Required flows: Specify which flows will be whitelisted for use in your RPC calls. -2. Service plugins: Register your services (see below). -3. Web APIs: You may register your own endpoints under /api/ of the bundled web server. -4. Static web endpoints: You may register your own static serving directories for serving web content from the web server. -5. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part +1. Service plugins: Register your services (see below). +2. Web APIs: You may register your own endpoints under /api/ of the bundled web server. +3. Static web endpoints: You may register your own static serving directories for serving web content from the web server. +4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted. Services diff --git a/docs/source/event-scheduling.rst b/docs/source/event-scheduling.rst index 2eede8bbd9..b5df1bcf1c 100644 --- a/docs/source/event-scheduling.rst +++ b/docs/source/event-scheduling.rst @@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events: ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the - wallet, it can expect to be queried for the next activity when committed to the wallet. + wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be + annotated with ``@SchedulableFlow``. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. The important thing to remember is that in the current implementation, each node that is party to the transaction will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract @@ -90,10 +91,7 @@ business process and to take on those roles. That ``FlowLogic`` will be handed rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered for the interest rate before indicating an error. -.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to - instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as - part of our overall security sandboxing. - +.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate. As previously mentioned, we currently need a small network handler to assist with session setup until the work to automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt index e33319c094..4a7def4e87 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/FxTransactionBuildTutorial.kt @@ -8,7 +8,6 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.TransactionType import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash -import net.corda.core.crypto.sign import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party @@ -48,12 +47,12 @@ private fun gatherOurInputs(serviceHub: ServiceHub, notary: Party?): Pair>, Long> { // Collect cash type inputs val cashStates = serviceHub.vaultService.unconsumedStates() - // extract our key identity for convenience - val ourKey = serviceHub.myInfo.legalIdentity.owningKey + // extract our identity for convenience + val ourIdentity = serviceHub.myInfo.legalIdentity // Filter down to our own cash states with right currency and issuer val suitableCashStates = cashStates.filter { val state = it.state.data - (state.owner == ourKey) + (state.owner == ourIdentity) && (state.amount.token == amountRequired.token) } require(!suitableCashStates.isEmpty()) { "Insufficient funds" } @@ -90,12 +89,12 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) // Build and an output state for the counterparty - val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey) + val transferedFundsOutput = Cash.State(sellAmount, request.counterparty) if (residual > 0L) { // Build an output state for the residual change back to us val residualAmount = Amount(residual, sellAmount.token) - val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey) + val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity) return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput)) } else { return FxResponse(inputs, listOf(transferedFundsOutput)) @@ -140,7 +139,7 @@ class ForeignExchangeFlow(val tradeId: String, require(it.inputs.all { it.state.notary == notary }) { "notary of remote states must be same as for our states" } - require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner.owningKey }) { + require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) { "The inputs are not owned by the correct counterparty" } require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) { @@ -153,7 +152,7 @@ class ForeignExchangeFlow(val tradeId: String, >= remoteRequestWithNotary.amount.quantity) { "the provided inputs don't provide sufficient funds" } - require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity.owningKey }. + require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity }. map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) { "the provided outputs don't provide the request quantity" } @@ -195,8 +194,8 @@ class ForeignExchangeFlow(val tradeId: String, val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary) // Add the move commands and key to indicate all the respective owners and need to sign - val ourSigners = ourStates.inputs.map { it.state.data.owner }.toSet() - val theirSigners = theirStates.inputs.map { it.state.data.owner }.toSet() + val ourSigners = ourStates.inputs.map { it.state.data.owner.owningKey }.toSet() + val theirSigners = theirStates.inputs.map { it.state.data.owner.owningKey }.toSet() builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList()) // Build and add the inputs and outputs @@ -206,11 +205,9 @@ class ForeignExchangeFlow(val tradeId: String, builder.withItems(*theirStates.outputs.toTypedArray()) // We have already validated their response and trust our own data - // so we can sign - builder.signWith(serviceHub.legalIdentityKey) - // create a signed transaction, but pass false as parameter, because we know it is not fully signed - val signedTransaction = builder.toSignedTransaction(checkSufficientSignatures = false) - return signedTransaction + // so we can sign. Note the returned SignedTransaction is still not fully signed + // and would not pass full verification yet. + return serviceHub.signInitialTransaction(builder) } // DOCEND 3 } @@ -260,7 +257,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { } // assuming we have completed state and business level validation we can sign the trade - val ourSignature = serviceHub.legalIdentityKey.sign(proposedTrade.id) + val ourSignature = serviceHub.createSignature(proposedTrade) // send the other side our signature. send(source, ourSignature) diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt index ccb6829894..6c25dea3b7 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/WorkflowTransactionBuildTutorial.kt @@ -2,9 +2,12 @@ package net.corda.docs import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub @@ -64,10 +67,10 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState { val parties: List get() = listOf(source, counterparty) - override val participants: List get() = parties.map { it.owningKey } + override val participants: List get() = parties override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.containsAny(ourKeys) } + return participants.any { it.owningKey.containsAny(ourKeys) } } } @@ -131,9 +134,7 @@ class SubmitTradeApprovalFlow(val tradeId: String, .withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey))) tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can automatically sign as there is no untrusted data. - tx.signWith(serviceHub.legalIdentityKey) - // Convert to a SignedTransaction that we can send to the notary - val signedTx = tx.toSignedTransaction(false) + val signedTx = serviceHub.signInitialTransaction(tx) // Notarise and distribute. subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty))) // Return the initial state @@ -195,9 +196,9 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can sign this transaction immediately as we have already checked all the fields and the decision // is ultimately a manual one from the caller. - tx.signWith(serviceHub.legalIdentityKey) - // Convert to SignedTransaction we can pass around certain that it cannot be modified. - val selfSignedTx = tx.toSignedTransaction(false) + // As a SignedTransaction we can pass the data around certain that it cannot be modified, + // although we do require further signatures to complete the process. + val selfSignedTx = serviceHub.signInitialTransaction(tx) //DOCEND 2 // Send the signed transaction to the originator and await their signature to confirm val allPartySignedTx = sendAndReceive(newState.source, selfSignedTx).unwrap { @@ -253,7 +254,7 @@ class RecordCompletionFlow(val source: Party) : FlowLogic() { } // DOCEND 3 // Having verified the SignedTransaction passed to us we can sign it too - val ourSignature = serviceHub.legalIdentityKey.sign(completeTx.tx.id) + val ourSignature = serviceHub.createSignature(completeTx) // Send our signature to the other party. send(source, ourSignature) // N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt index b599d8f67b..5713a3041a 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/FxTransactionBuildTutorialTest.kt @@ -8,7 +8,6 @@ import net.corda.core.toFuture import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.flows.CashIssueFlow -import net.corda.flows.CashPaymentFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.transaction diff --git a/docs/source/flow-library.rst b/docs/source/flow-library.rst new file mode 100644 index 0000000000..1110dbc813 --- /dev/null +++ b/docs/source/flow-library.rst @@ -0,0 +1,63 @@ +Flow Library +============ + +There are a number of built-in flows supplied with Corda, which cover some core functionality. + +FinalityFlow +------------ + +The ``FinalityFlow`` verifies the given transactions, then sends them to the specified notary. + +If the notary agrees that the transactions are acceptable then they are from that point onwards committed to the ledger, +and will be written through to the vault. Additionally they will be distributed to the parties reflected in the participants +list of the states. + +The transactions will be topologically sorted before commitment to ensure that dependencies are committed before +dependers, so you don't need to do this yourself. + +The transactions are expected to have already been resolved: if their dependencies are not available in local storage or +within the given set, verification will fail. They must have signatures from all necessary parties other than the notary. + +If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each +transaction are calculated on a per transaction basis from the contract-given set of participants. + +The flow returns the same transactions, in the same order, with the additional signatures. + + +CollectSignaturesFlow +--------------------- + +The ``CollectSignaturesFlow`` is used to automate the collection of signatures from the counter-parties to a transaction. + +You use the ``CollectSignaturesFlow`` by passing it a ``SignedTransaction`` which has at least been signed by yourself. +The flow will handle the resolution of the counter-party identities and request a signature from each counter-party. + +Finally, the flow will verify all the signatures and return a ``SignedTransaction`` with all the collected signatures. + +When using this flow on the responding side you will have to subclass the ``AbstractCollectSignaturesFlowResponder`` and +provide your own implementation of the ``checkTransaction`` method. This is to add additional verification logic on the +responder side. Types of things you will need to check include: + +* Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the expected + type of inputs and outputs +* Checking that the properties of the outputs are as you would expect, this is in the absence of integrating reference + data sources to facilitate this for us +* Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as potentially + the transaction creator has access to some of your state references + +Typically after calling the ``CollectSignaturesFlow`` you then called the ``FinalityFlow``. + +ResolveTransactionsFlow +----------------------- + +This ``ResolveTransactionsFlow`` is used to verify the validity of a transaction by recursively checking the validity of +all the dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be +checked again. + +A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that +transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted +into the database as long as a [SignedTransaction] was provided. If only the ``WireTransaction`` form was provided +then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way +to use the flow is helpful when resolving and verifying an unfinished transaction. + +The flow returns a list of verified ``LedgerTransaction`` objects, in a depth-first order. \ No newline at end of file diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index abe1afa90d..9e90f045cd 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -131,7 +131,7 @@ each side. val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val myKeyPair: KeyPair, + val myKey: PublicKey, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -160,7 +160,8 @@ Going through the data needed to become a seller, we have: information on notaries. - ``assetToSell: StateAndRef`` - a pointer to the ledger entry that represents the thing being sold. - ``price: Amount`` - the agreed on price that the asset is being sold for (without an issuer constraint). -- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction. +- ``myKey: PublicKey`` - the PublicKey part of the node's internal KeyPair that controls the asset being sold. +The matching PrivateKey stored in the KeyManagementService will be used to sign the transaction. And for the buyer: @@ -206,9 +207,10 @@ how to register handlers with the messaging system (see ":doc:`messaging`") and when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network interaction and it will save/restore serialised versions of the fiber at the right times. -Flows can be invoked in several ways. For instance, they can be triggered by scheduled events, -see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC -APIs from your app code. +Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to +be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered +directly via the node's RPC API from your app code (in which case they need to be annotated with `StartableByRPC`). It's +possible for a flow to be of both types. You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``). @@ -399,15 +401,35 @@ This code is longer but no more complicated. Here are some things to pay attenti As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite the fact that it takes minimal resources and can survive node restarts. -Initiating communication ------------------------- +Flow sessions +------------- -Now that we have both sides of the deal negotation implemented as flows we need a way to start things off. We do this by -having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using -RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example -it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller`` -are annotated with it. For example, if we choose the seller side as the initiator then we need a seller starter flow that -might look something like this: +Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows +running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore +flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes +do not interfere with each other. + +To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party`` +for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to +the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party`` +will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends +pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known +sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is +``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate. + +Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication +channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is +making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp +which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method +specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist +then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be +annotated with ``InitiatingFlow``. + +Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done +with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side. +In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor +``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need +to create a simple seller starter flow that has the annotation we need: .. container:: codeset @@ -418,7 +440,7 @@ might look something like this: @Suspendable override fun call(): SignedTransaction { val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey: KeyPair = serviceHub.legalIdentityKey + val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey)) } } diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index c7a5f95d7e..690b779374 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -4,63 +4,142 @@ Getting set up Software requirements --------------------- -Corda uses industry-standard tools to make set-up as simple as possible. Following the software recommendations below will -minimize the number of errors you encounter, and make it easier for others to provide support. However, if you do use other tools, -we're interested to hear about any issues that arise. +Corda uses industry-standard tools to make set-up as simple as possible. Following the software recommendations below will minimize the number of errors you encounter, and make it easier for others to provide support. However, if you do use other tools, we'd be interested to hear about any issues that arise. JVM ~~~ -Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively -supported. Oracle JDK 8 can be obtained directly from -`Oracle `_. Installation instructions are -available for `Windows `_, -`Linux `_ and -`OS X `_. +Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively supported. -Please ensure that you keep your Oracle JDK installation updated to the latest version while working with Corda. -Even earlier versions of JDK 8 versions can cause cryptic errors. +Please ensure that you keep your Oracle JDK installation updated to the latest version while working with Corda. Even earlier versions of JDK 8 versions can cause cryptic errors. If you do choose to use OpenJDK instead of Oracle's JDK, you will also need to install OpenJFX. -Additional troubleshooting information can be found `here `_. - Kotlin ~~~~~~ -Applications on Corda (CorDapps) can be written in any JVM-targeting language. However, Corda itself and most of the samples -are written in Kotlin. If you're unfamiliar with Kotlin, there is an official `getting started guide `_. +Applications on Corda (CorDapps) can be written in any JVM-targeting language. However, Corda itself and most of the samples are written in Kotlin. If you're unfamiliar with Kotlin, there is an official `getting started guide `_. + See also our :doc:`further-notes-on-kotlin`. IDE ~~~ -We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration. The free Community -Edition can be downloaded from `JetBrains `_. +We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration. -Please make sure that you're running the latest version of IDEA, as older versions have been known to have problems integrating with Gradle, -the build tool used by Corda. - -You'll also want to install the Kotlin IDEA plugin by following the instructions -`here `_. - -Additional troubleshooting information can be found `here `_. +Please make sure that you're running the latest version of IDEA, as older versions have been known to have problems integrating with Gradle, the build tool used by Corda. Git ~~~ -We use git to version-control Corda. Instructions on installing git can be found -`here `_. - -Following these instructions will give you access to git via the command line. It can also be useful to control git via IDEA. Instructions -for doing so can be found on the `JetBrains website `_. +We use git to version-control Corda. Gradle ~~~~~~ We use Gradle as the build tool for Corda. However, you do not need to install Gradle itself, as a wrapper is provided. -The wrapper can be run from the command line by using ``./gradlew [taskName]`` on OS X/Linux, or ``gradlew.bat [taskName]`` on Windows. +Set-up instructions +------------------- + +The instructions below will allow you to set up a Corda development environment and run a basic CorDapp on a Windows or Mac machine. If you have any issues, please consult the :doc:`getting-set-up-fault-finding` page, or reach out on `Slack `_ or the `forums `_. + +.. note:: The set-up instructions are also available in video form for both `Windows `_ and `Mac `_. + +Windows +~~~~~~~ + +Java +"""" +1. Visit http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html +2. Scroll down to "Java SE Development Kit 8uXXX" (where "XXX" is the latest minor version number) +3. Toggle "Accept License Agreement" +4. Click the download link for jdk-8uXXX-windows-x64.exe (where "XXX" is the latest minor version number) +5. Download and run the executable to install Java (use the default settings) +6. Open a new command prompt and run ``java -version`` to test that Java is installed correctly + +Git +""" +1. Visit https://git-scm.com/download/win +2. Click the "64-bit Git for Windows Setup" download link. +3. Download and run the executable to install Git (use the default settings) +4. Open a new command prompt and type ``git --version`` to test that git is installed correctly + +IntelliJ +"""""""" +1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?code=IIC +2. Download and run the executable to install IntelliJ Community Edition (use the default settings) + +Download a sample project +""""""""""""""""""""""""" +1. Open a command prompt +2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` +3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` +4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*`` +5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) + +Run from the command prompt +""""""""""""""""""""""""""" +1. From the cordapp-tutorial folder, deploy the nodes by running ``gradlew deployNodes`` +2. Start the nodes by running ``call kotlin-source/build/nodes/runnodes.bat`` +3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" +4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ + +Run from IntelliJ +""""""""""""""""" +1. Open IntelliJ Community Edition +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-template folder + +.. warning:: If you click "Import Project" instead of "Open", the project's run configurations will be erased! + +3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to C:\Program Files\Java\jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK". +4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears +5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete) +6. At the top-right of the screen, to the left of the green "play" arrow, you should see a dropdown. In that dropdown, select "Run Example Cordapp - Kotlin" and click the green "play" arrow. +7. Wait until the run windows displays the message "Webserver started up in XX.X sec" +8. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ + +Mac +~~~ + +Java +"""" +1. Open "System Preferences > Java" +2. In the Java Control Panel, if an update is available, click "Update Now" +3. In the "Software Update" window, click "Install Update". If required, enter your password and click "Install Helper" when prompted +4. Wait for a pop-up window indicating that you have successfully installed the update, and click "Close" +5. Open a new terminal and type ``java -version`` to test that Java is installed correctly + +IntelliJ +"""""""" +1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?platform=mac&code=IIC +2. Download and run the executable to install IntelliJ Community Edition (use the default settings) + +Download a sample project +""""""""""""""""""""""""" +1. Open a terminal +2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial`` +3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial`` +4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*`` +5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone) + +Run from the terminal +""""""""""""""""""""" +1. From the cordapp-tutorial folder, deploy the nodes by running ``./gradlew deployNodes`` +2. Start the nodes by running ``kotlin-source/build/nodes/runnodes``. Do not click while 8 additional terminal windows start up. +3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec" +4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ + +Run from IntelliJ +""""""""""""""""" +1. Open IntelliJ Community Edition +2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-template folder +3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to /Library/Java/JavaVirtualMachines/jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK". +4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears +5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete) +6. At the top-right of the screen, to the left of the green "play" arrow, you should see a dropdown. In that dropdown, select "Run Example Cordapp - Kotlin" and click the green "play" arrow. +7. Wait until the run windows displays the message "Webserver started up in XX.X sec" +8. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/ Corda source code ----------------- @@ -79,25 +158,7 @@ And a simple example CorDapp for you to explore basic concepts is available here You can clone these repos to your local machine by running the command ``git clone [repo URL]``. -By default, these repos will be on the ``master`` branch. However, this is an unstable development branch. You should check -out the latest release tag instead by running ``git checkout release-M10.1``. - -Opening Corda/CorDapps in IDEA -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. warning:: If you choose to use IntelliJ you must run the ``gradlew kaptKotlin`` task before attempting to compile via IntelliJ. - -.. note:: If you change branch , gradle clean or see a compile error in ``VaultSchemaTest.kt`` you must also then re-run `gradlew kaptKotlin` - -When opening a Corda project for the first time from the IDEA splash screen, please click "Open" rather than "Import Project", -and then import the Gradle project by clicking "Import Gradle project" in the popup bubble on the lower right-hand side of the screen. -If you instead pick "Import Project" on the splash screen, a bug in IDEA will cause Corda's pre-packaged run configurations to be erased. - -If you see this warning too late, that's not a problem - just use ``git checkout .idea/runConfigurations`` or the version control tab in -IDEA to undelete the files. - -IDEA's build of the project may need to be resynced from time to time. This can be done from within IDEA by going to "View" -> "Tool Windows" -> "Gradle" -and clicking "Refresh all Gradle projects". Whenever prompted about Gradle, accept the defaults suggested by IDEA. +By default, these repos will be on the unstable ``master`` branch. You should check out the latest milestone release instead by running ``git checkout release-M11.1``. Next steps ---------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 12746d462c..0fa0e49946 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -2,10 +2,10 @@ Welcome to the Corda documentation! =================================== .. warning:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the - current state of the code. `Read the docs for milestone release M10.1 `_. + current state of the code. `Read the docs for milestone release M11.1 `_. `Corda `_ is an open-source distributed ledger platform. The latest *milestone* (i.e. stable) -release is M10.1. The codebase is on `GitHub `_, and our community can be found on +release is M11.1. The codebase is on `GitHub `_, and our community can be found on `Slack `_ and in our `forum `_. If you're new to Corda, you should start by learning about its motivating vision and architecture. A good introduction @@ -118,6 +118,7 @@ Documentation Contents: :maxdepth: 2 :caption: Component library + flow-library contract-catalogue contract-irs diff --git a/docs/source/key-concepts-core-types.rst b/docs/source/key-concepts-core-types.rst index 724861ac44..d76da267d8 100644 --- a/docs/source/key-concepts-core-types.rst +++ b/docs/source/key-concepts-core-types.rst @@ -55,10 +55,9 @@ resolving the attachment references to the attachments. Commands with valid sign When constructing a new transaction from scratch, you use ``TransactionBuilder``, which is a mutable transaction that can be signed once its construction is complete. This builder class should be used to create the initial transaction representation (before signature, before verification). It is intended to be passed around code that may edit it by adding new states/commands. -Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties. -It is typical for contract classes to expose helper methods that can contribute to a ``TransactionBuilder``. Once a transaction -has been constructed using the builders ``toWireTransaction`` or ``toSignedTransaction`` function, it shared with other -participants using the :doc:`key-concepts-flow-framework`. +Then once the states and commands are right then an initial DigitalSignature.WithKey can be added to freeze the transaction data. +Typically, the signInitialTransaction method on the flow's serviceHub object will be used to look up the default node identity PrivateKey, +sign the transaction and return a partially signed SignedTransaction. This can then be distributed to other participants using the :doc:`key-concepts-flow-framework`. Here's an example of building a transaction that creates an issuance of bananas (note that bananas are not a real contract type in the library): @@ -69,10 +68,9 @@ contract type in the library): val notaryToUse: Party = ... val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia")) - txb.signWith(myKey) txb.setTime(Instant.now(), notaryToUse, 30.seconds) - // We must disable the check for sufficient signatures, because this transaction is not yet notarised. - val stx = txb.toSignedTransaction(checkSufficientSignatures = false) + // Carry out the initial signing of the transaction and creation of a (partial) SignedTransation. + val stx = serviceHub.signInitialTransaction(txb) // Alternatively, let's just check it verifies pretending it was fully signed. To do this, we get // a WireTransaction, which is what the SignedTransaction wraps. Thus by verifying that directly we // skip signature checking. diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index f2675129d4..d54a13b936 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -1,21 +1,14 @@ Overview ======== -This section describes the fundamental concepts and features that underpin the Corda platform, to include: +This section describes the key concepts and features of the Corda platform. - * :doc:`key-concepts-ecosystem` - * :doc:`key-concepts-data-model` - * :doc:`key-concepts-core-types` - * :doc:`key-concepts-financial-model` - * :doc:`key-concepts-flow-framework` - * :doc:`key-concepts-consensus-notaries` - * :doc:`key-concepts-vault` - * :doc:`key-concepts-security-model` - -Detailed thinking and rationale behind these concepts are presented in the following published white papers: +The detailed thinking and rationale behind these concepts are presented in two white papers: * `Corda: An Introduction`_ - * `Corda: A Distributed Ledger`_ (Technical White Paper) + * `Corda: A Distributed Ledger`_ (A.K.A. the Technical White Paper) + +Explanations of the key concepts are also available as `videos `_. .. _`Corda: An Introduction`: _static/corda-introductory-whitepaper.pdf .. _`Corda: A Distributed Ledger`: _static/corda-technical-whitepaper.pdf diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 578fc6c023..059f8e0070 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -65,12 +65,12 @@ PersistentKeyManagementService and E2ETestKeyManagementService Typical usage of these services is to locate an appropriate ``PrivateKey`` to complete and sign a verified transaction as part of a flow. The normal node legal identifier keys are typically accessed via -helper extension methods on the ``ServiceHub``, but these ultimately -fetch the keys from the ``KeyManagementService``. The +helper extension methods on the ``ServiceHub``, but these ultimately delegate +signing to internal ``PrivateKeys`` from the ``KeyManagementService``. The ``KeyManagementService`` interface also allows other keys to be generated if anonymous keys are needed in a flow. Note that this -interface works at the level of individual ``PublicKey``/``PrivateKey`` -pairs, but the signing authority will be represented by a +interface works at the level of individual ``PublicKey`` and internally +matched ``PrivateKey` pairs, but the signing authority may be represented by a ``CompositeKey`` on the ``NodeInfo`` to allow key clustering and threshold schemes. diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 63f2f8fef2..63afdf510f 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -168,11 +168,12 @@ Let's see what parameters we pass to the constructor of this oracle. .. sourcecode:: kotlin - class Oracle(val identity: Party, private val signingKey: KeyPair, val clock: Clock) = TODO() + class Oracle(val identity: Party, private val signingKey: PublicKey, val clock: Clock) = TODO() Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to -sign for, and also needs a pair of signing keys with which it signs transactions. The clock is used for the deadline -functionality which we will not discuss further here. +sign for, and also needs the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService +to identify the internal PrivateKey used for transaction signing. +The clock is used for the deadline functionality which we will not discuss further here. Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the parameter and ``CommandData`` classes. diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 47427afd40..02a2ab14cf 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -10,7 +10,8 @@ We've added the ability for flows to be versioned by their CorDapp developers. T version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future release we allow a node to have multiple versions of the same flow running to enable backwards compatibility. -There are major changes to the ``Party`` class as part of confidential identities. See :doc:`changelog` for full details. +There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored +in transaction state objects. See :doc:`changelog` for full details. Milestone 11 diff --git a/docs/source/release-process.rst b/docs/source/release-process.rst deleted file mode 100644 index 04f90fc1a1..0000000000 --- a/docs/source/release-process.rst +++ /dev/null @@ -1,48 +0,0 @@ -Release process -=============== - -Corda is under heavy development. The current release process is therefore geared towards rapid iteration. - -Each Corda development release is called a *milestone* and has its own branch in the git repository. Milestones are -temporarily stabilised snapshots of the Corda code which are suitable for developers to experiment with. They may -receive backported bugfixes but once announced a milestone will not have any API or backwards compatibility breaks. - -Between milestones backwards compatibility is expected to break. Every new milestone comes with a short announcement -detailing: - -* What major improvements have been made. -* How to forward port your code to the new milestone. -* What new documentation has become available. -* Important known issues. - -Eventually, Corda will stabilise and release version 1. At that point backwards compatibility will be guaranteed -forever and the software will be considered production ready. Until then, expect it to be a building site and wear your -hard hat. - -Our goal is to cut a new milestone roughly once a month. There are no fixed dates. If need be, a milestone may slip by -a few days to ensure the code is sufficiently usable. Usually the release will happen around the end of the month. - -Steps to cut a release ----------------------- - -1. Pick a commit that is stable and do basic QA: run all the tests, run the demos. -2. Review the commits between this release and the last looking for new features, API changes, etc. Make sure the - summary in the current section of the :doc:`changelog` is correct and update if not. Then move it into the right - section for this release. -3. Write up a summary of the changes for the :doc:`release-notes`. This should primarily be suited to a semi-technical - audience, but any advice on how to port app code from the previous release, configuration changes required, etc. - should also go here. -4. Additionally, if there are any new features or APIs that deserve a new section in the docsite and the author didn't - create one, bug them to do so a day or two before the release. -5. Regenerate the docsite if necessary and commit. -6. Create a branch with a name like `release-M0` where 0 is replaced by the number of the milestone. -7. Adjust the version in the root build.gradle file to take out the -SNAPSHOT and commit it on the branch. -8. Remove the "is master" warning from the docsite index page on this branch only. -9. Tag the branch with a tag like `release-M0.0` -10. Push the branch and the tag to git. -11. Write up a short announcement containing the summary of new features, changes, and API breaks. - This can often be derived from the release notes. Send it to the r3dlg-awg mailing list. -12. On master, adjust the version number in the root build.gradle file upwards. - -If there are serious bugs found in the release, backport the fix to the branch and then tag it with e.g. `release-M0.1` -Minor changes to the branch don't have to be announced unless it'd be critical to get all developers updated. diff --git a/docs/source/resources/Yo_peers.png b/docs/source/resources/Yo_peers.png new file mode 100644 index 0000000000..2629a87806 Binary files /dev/null and b/docs/source/resources/Yo_peers.png differ diff --git a/docs/source/resources/Yo_web_ui.png b/docs/source/resources/Yo_web_ui.png new file mode 100644 index 0000000000..e0b1d8b1fb Binary files /dev/null and b/docs/source/resources/Yo_web_ui.png differ diff --git a/docs/source/resources/azure_ip.png b/docs/source/resources/azure_ip.png new file mode 100644 index 0000000000..f8dc87e538 Binary files /dev/null and b/docs/source/resources/azure_ip.png differ diff --git a/docs/source/resources/azure_multi_node_step1.png b/docs/source/resources/azure_multi_node_step1.png new file mode 100644 index 0000000000..7bfdee9c9e Binary files /dev/null and b/docs/source/resources/azure_multi_node_step1.png differ diff --git a/docs/source/resources/azure_multi_node_step2.png b/docs/source/resources/azure_multi_node_step2.png new file mode 100644 index 0000000000..13ed2bf0a5 Binary files /dev/null and b/docs/source/resources/azure_multi_node_step2.png differ diff --git a/docs/source/resources/azure_multi_node_step3.png b/docs/source/resources/azure_multi_node_step3.png new file mode 100644 index 0000000000..5bdd072ce9 Binary files /dev/null and b/docs/source/resources/azure_multi_node_step3.png differ diff --git a/docs/source/resources/azure_multi_node_step4.png b/docs/source/resources/azure_multi_node_step4.png new file mode 100644 index 0000000000..bbd300f652 Binary files /dev/null and b/docs/source/resources/azure_multi_node_step4.png differ diff --git a/docs/source/resources/azure_syslog.png b/docs/source/resources/azure_syslog.png new file mode 100644 index 0000000000..08dbdd3f59 Binary files /dev/null and b/docs/source/resources/azure_syslog.png differ diff --git a/docs/source/resources/azure_yos.png b/docs/source/resources/azure_yos.png new file mode 100644 index 0000000000..8065c1b0bb Binary files /dev/null and b/docs/source/resources/azure_yos.png differ diff --git a/docs/source/resources/corda_banner.png b/docs/source/resources/corda_banner.png new file mode 100644 index 0000000000..9118c3e353 Binary files /dev/null and b/docs/source/resources/corda_banner.png differ diff --git a/docs/source/resources/yo_flow_progress.png b/docs/source/resources/yo_flow_progress.png new file mode 100644 index 0000000000..fbd0f5884f Binary files /dev/null and b/docs/source/resources/yo_flow_progress.png differ diff --git a/docs/source/resources/yo_peers2.png b/docs/source/resources/yo_peers2.png new file mode 100644 index 0000000000..9212981c5d Binary files /dev/null and b/docs/source/resources/yo_peers2.png differ diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 69b297c68c..32fb39c465 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -3,13 +3,13 @@ Client RPC API tutorial ======================= -In this tutorial we will build a simple command line utility that -connects to a node, creates some Cash transactions and meanwhile dumps -the transaction graph to the standard output. We will then put some -simple visualisation on top. For an explanation on how the RPC works -see :doc:`clientrpc`. +In this tutorial we will build a simple command line utility that connects to a node, creates some Cash transactions and +meanwhile dumps the transaction graph to the standard output. We will then put some simple visualisation on top. For an +explanation on how the RPC works see :doc:`clientrpc`. -We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes directly. +We start off by connecting to the node itself. For the purposes of the tutorial we will use the Driver to start up a notary +and a node that issues/exits and moves Cash around for herself. To authenticate we will use the certificates of the nodes +directly. Note how we configure the node to create a user that has permission to start the CashFlow. @@ -25,14 +25,16 @@ Now we can connect to the node itself using a valid RPC login. We login using th :start-after: START 2 :end-before: END 2 -We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, which exposes the full RPC interface of the node: +We start generating transactions in a different thread (``generateTransactions`` to be defined later) using ``proxy``, +which exposes the full RPC interface of the node: .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt :language: kotlin :start-after: interface CordaRPCOps :end-before: } -.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made available to RPC clients. +.. warning:: This API is evolving and will continue to grow as new functionality and features added to Corda are made + available to RPC clients. The one we need in order to dump the transaction graph is ``verifiedTransactions``. The type signature tells us that the RPC will return a list of transactions and an Observable stream. This is a general pattern, we query some data and the @@ -61,13 +63,19 @@ Now we just need to create the transactions themselves! :start-after: START 6 :end-before: END 6 -We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` or mark it as not used. Failing to do this will leak resources in the node.) +We utilise several RPC functions here to query things like the notaries in the node cluster or our own vault. These RPC +functions also return ``Observable`` objects so that the node can send us updated values. However, we don't need updates +here and so we mark these observables as ``notUsed``. (As a rule, you should always either subscribe to an ``Observable`` +or mark it as not used. Failing to do this will leak resources in the node.) Then in a loop we generate randomly either an Issue, a Pay or an Exit transaction. -The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that type-checks the arguments for us. +The RPC we need to initiate a Cash transaction is ``startFlowDynamic`` which may start an arbitrary flow, given sufficient +permissions to do so. We won't use this function directly, but rather a type-safe wrapper around it ``startFlow`` that +type-checks the arguments for us. -Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while listening on successfully created ones, which are dumped to the console. We just need to run it!: +Finally we have everything in place: we start a couple of nodes, connect to them, and start creating transactions while +listening on successfully created ones, which are dumped to the console. We just need to run it!: .. code-block:: text @@ -106,9 +114,10 @@ RPC credentials associated with a Client must match the permission set configure This refers to both authentication (username and password) and role-based authorisation (a permissioned set of RPC operations an authenticated user is entitled to run). -.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. - Currently the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may execute. - An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to any flow. +.. note:: Permissions are represented as *String's* to allow RPC implementations to add their own permissioning. Currently + the only permission type defined is *StartFlow*, which defines a list of whitelisted flows an authenticated use may + execute. An administrator user (or a developer) may also be assigned the ``ALL`` permission, which grants access to + any flow. In the instructions above the server node permissions are configured programmatically in the driver code: @@ -126,7 +135,8 @@ When starting a standalone node using a configuration file we must supply the RP { username=user, password=password, permissions=[ StartFlow.net.corda.flows.CashFlow ] } ] -When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar manner: +When using the gradle Cordformation plugin to configure and deploy a node you must supply the RPC credentials in a similar +manner: .. code-block:: text @@ -148,5 +158,8 @@ You can then deploy and launch the nodes (Notary and Alice) as follows: ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Print ./docs/source/example-code/build/install/docs/source/example-code/bin/client-rpc-tutorial Visualise +With regards to the start flow RPCs, there is an extra layer of security whereby the flow to be executed has to be +annotated with ``@StartableByRPC``. Flows without this annotation cannot execute using RPC. + See more on security in :doc:`secure-coding-guidelines`, node configuration in :doc:`corda-configuration-file` and Cordformation in :doc:`creating-a-cordapp` diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 25802e3c26..8fe5836d4a 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -704,10 +704,10 @@ from the ledger). Finally, we add a Redeem command that should be signed by the versions with a more complete way to express issuer constraints. A ``TransactionBuilder`` is not by itself ready to be used anywhere, so first, we must convert it to something that -is recognised by the network. The most important next step is for the participating entities to sign it using the -``signWith()`` method. This takes a keypair, serialises the transaction, signs the serialised form and then stores the -signature inside the ``TransactionBuilder``. Once all parties have signed, you can call ``TransactionBuilder.toSignedTransaction()`` -to get a ``SignedTransaction`` object. +is recognised by the network. The most important next step is for the participating entities to sign it. Typically, +an initiating flow will create an initial partially signed ``SignedTransaction`` by calling the ``serviceHub.signInitialTransaction`` method. +Then the frozen ``SignedTransaction`` can be passed to other nodes by the flow, these can sign using ``serviceHub.createSignature`` and distribute. +The ``CollectSignaturesFlow`` provides a generic implementation of this process that can be used as a ``subFlow`` . You can see how transactions flow through the different stages of construction by examining the commercial paper unit tests. diff --git a/docs/source/using-a-notary.rst b/docs/source/using-a-notary.rst index eefa475a3f..650755247b 100644 --- a/docs/source/using-a-notary.rst +++ b/docs/source/using-a-notary.rst @@ -63,9 +63,8 @@ We then sign the transaction, build and record it to our transaction storage: .. sourcecode:: kotlin - val mySigningKey: KeyPair = serviceHub.legalIdentityKey - builder.signWith(mySigningKey) - val issueTransaction = builder.toSignedTransaction() + val mySigningKey: PublicKey = serviceHub.legalIdentityKey + val issueTransaction = serviceHub.signInitialTransaction(issueTransaction, mySigningKey) serviceHub.recordTransactions(issueTransaction) The transaction is recorded and we now have a state (asset) in possession that we can transfer to someone else. Note @@ -97,9 +96,9 @@ Again we sign the transaction, and build it: .. sourcecode:: kotlin - moveTransactionBuilder.signWith(mySigningKey) - // We build it without checking if all signatures are present, because we know that the notary signature is missing - val moveTransaction = builder.toSignedTransaction(checkSufficientSignatures = false) + // We build it and add our default identity signature without checking if all signatures are present, + // Note we know that the notary signature is missing, so thie SignedTransaction is still partial. + val moveTransaction = serviceHub.signInitialTransaction(moveTransactionBuilder) Next we need to obtain a signature from the notary for the transaction to be valid. Prior to signing, the notary will commit our old (input) state so it cannot be used again. diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt index 905ec3f7a5..a266b37fb4 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/DoormanWebService.kt @@ -3,7 +3,7 @@ package com.r3.corda.doorman import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestStorage -import net.corda.core.crypto.CertificateAndKey +import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA @@ -26,7 +26,7 @@ import javax.ws.rs.core.Response.Status.UNAUTHORIZED * Provides functionality for asynchronous submission of certificate signing requests and retrieval of the results. */ @Path("") -class DoormanWebService(val intermediateCACertAndKey: CertificateAndKey, val rootCert: Certificate, val storage: CertificationRequestStorage, val serverStatus: DoormanServerStatus) { +class DoormanWebService(val intermediateCACertAndKey: CertificateAndKeyPair, val rootCert: Certificate, val storage: CertificationRequestStorage, val serverStatus: DoormanServerStatus) { @Context lateinit var request: HttpServletRequest /** * Accept stream of [PKCS10CertificationRequest] from user and persists in [CertificationRequestStorage] for approval. diff --git a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt index 02092137b5..865164241b 100644 --- a/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt +++ b/doorman/src/main/kotlin/com/r3/corda/doorman/Main.kt @@ -15,7 +15,7 @@ import net.corda.core.crypto.X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA_PRIVATE_KEY import net.corda.core.crypto.X509Utilities.createIntermediateCert -import net.corda.core.crypto.X509Utilities.createServerCert +import net.corda.core.crypto.X509Utilities.createTlsServerCert import net.corda.core.seconds import net.corda.core.utilities.loggerFor import net.corda.node.utilities.configureDatabase @@ -42,7 +42,7 @@ import kotlin.system.exitProcess * The server will require keystorePath, keystore password and key password via command line input. * The Intermediate CA certificate,Intermediate CA private key and Root CA Certificate should use alias name specified in [X509Utilities] */ -class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAndKey, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { +class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAndKeyPair, val rootCACert: Certificate, val storage: CertificationRequestStorage) : Closeable { val serverStatus = DoormanServerStatus() companion object { @@ -83,7 +83,7 @@ class DoormanServer(webServerAddr: HostAndPort, val caCertAndKey: CertificateAnd for (id in storage.getApprovedRequestIds()) { storage.approveRequest(id) { val request = JcaPKCS10CertificationRequest(request) - createServerCert(request.subject, request.publicKey, caCertAndKey, + createTlsServerCert(request.subject, request.publicKey, caCertAndKey, if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) } logger.info("Approved request $id") @@ -159,7 +159,7 @@ private fun DoormanParameters.generateCAKeyPair() { val rootPrivateKeyPassword = rootPrivateKeyPassword ?: readPassword("Root Private Key Password: ") val rootKeyStore = loadKeyStore(rootStorePath, rootKeystorePassword) - val rootKeyAndCert = rootKeyStore.getCertificateAndKey(rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) + val rootKeyAndCert = rootKeyStore.getCertificateAndKeyPair(rootPrivateKeyPassword, CORDA_ROOT_CA_PRIVATE_KEY) val keystorePassword = keystorePassword ?: readPassword("Keystore Password: ") val caPrivateKeyPassword = caPrivateKeyPassword ?: readPassword("CA Private Key Password: ") @@ -190,7 +190,7 @@ private fun DoormanParameters.startDoorman() { val keystore = loadOrCreateKeyStore(keystorePath, keystorePassword) val rootCACert = keystore.getCertificateChain(CORDA_INTERMEDIATE_CA_PRIVATE_KEY).last() - val caCertAndKey = keystore.getCertificateAndKey(caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY) + val caCertAndKey = keystore.getCertificateAndKeyPair(caPrivateKeyPassword, CORDA_INTERMEDIATE_CA_PRIVATE_KEY) // Create DB connection. val (datasource, database) = configureDatabase(dataSourceProperties) diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt index 2e8068fa2a..a05648adab 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/DoormanServiceTest.kt @@ -5,7 +5,10 @@ import com.nhaarman.mockito_kotlin.* import com.r3.corda.doorman.persistence.CertificateResponse import com.r3.corda.doorman.persistence.CertificationRequestData import com.r3.corda.doorman.persistence.CertificationRequestStorage -import net.corda.core.crypto.* +import net.corda.core.crypto.CertificateStream +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat @@ -87,7 +90,7 @@ class DoormanServiceTest { storage.approveRequest(id) { JcaPKCS10CertificationRequest(request).run { - X509Utilities.createServerCert(subject, publicKey, intermediateCA, + X509Utilities.createTlsServerCert(subject, publicKey, intermediateCA, if (ipAddress == hostName) listOf() else listOf(hostName), listOf(ipAddress)) } } diff --git a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt index a5d722a85d..3c73cdb555 100644 --- a/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt +++ b/doorman/src/test/kotlin/com/r3/corda/doorman/internal/persistence/DBCertificateRequestStorageTest.kt @@ -137,7 +137,7 @@ class DBCertificateRequestStorageTest { private fun approveRequest(requestId: String) { storage.approveRequest(requestId) { JcaPKCS10CertificationRequest(request).run { - X509Utilities.createServerCert( + X509Utilities.createTlsServerCert( subject, publicKey, intermediateCA, diff --git a/experimental/quasar-hook/README.md b/experimental/quasar-hook/README.md new file mode 100644 index 0000000000..43a253a5af --- /dev/null +++ b/experimental/quasar-hook/README.md @@ -0,0 +1,21 @@ +What is this? +============= + +This is a javaagent that may be used while running applications using quasar. It hooks into quasar to track what +methods are scanned, instrumented and used at runtime, and generates an exclude pattern that may be passed in to quasar +to stop it from scanning classes unnecessarily. + +Example usage +============= + +``` +./gradlew experimental:quasar-hook:jar +java -javaagent:experimental/quasar-hook/build/libs/quasar-hook.jar="expand=com,de,org,co;truncate=net.corda" -jar path/to/corda.jar +``` + +The above will run corda.jar and on exit will print information about what classes were scanned/instrumented. + +`expand` and `truncate` tweak the output exclude pattern. `expand` is a list of packages to always expand (for example +instead of generating `com.*` generate `com.google.*,com.typesafe.*` etc.), `truncate` is a list of packages that should +not be included in the exclude pattern. Truncating `net.corda` means nothing should be excluded from instrumentation in +Corda. \ No newline at end of file diff --git a/experimental/quasar-hook/build.gradle b/experimental/quasar-hook/build.gradle new file mode 100644 index 0000000000..08b10c30a0 --- /dev/null +++ b/experimental/quasar-hook/build.gradle @@ -0,0 +1,51 @@ +buildscript { + // For sharing constants between builds + Properties constants = new Properties() + file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } + + ext.kotlin_version = constants.getProperty("kotlinVersion") + ext.javaassist_version = "3.12.1.GA" + + repositories { + mavenLocal() + mavenCentral() + jcenter() + } + + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +repositories { + mavenLocal() + mavenCentral() + jcenter() +} + +apply plugin: 'kotlin' +apply plugin: 'kotlin-kapt' +apply plugin: 'idea' + +description 'A javaagent to allow hooking into the instrumentation by Quasar' + +dependencies { + compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" + compile "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" + compile "javassist:javassist:$javaassist_version" +} + +jar { + archiveName = "${project.name}.jar" + manifest { + attributes( + 'Premain-Class': 'net.corda.quasarhook.QuasarInstrumentationHookAgent', + 'Can-Redefine-Classes': 'true', + 'Can-Retransform-Classes': 'true', + 'Can-Set-Native-Method-Prefix': 'true', + 'Implementation-Title': "QuasarHook", + 'Implementation-Version': rootProject.version + ) + } + from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } +} diff --git a/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt new file mode 100644 index 0000000000..960688379b --- /dev/null +++ b/experimental/quasar-hook/src/main/kotlin/net/corda/quasarhook/QuasarInstrumentationHook.kt @@ -0,0 +1,288 @@ +package net.corda.quasarhook + +import javassist.ClassPool +import javassist.CtClass +import java.io.ByteArrayInputStream +import java.lang.instrument.ClassFileTransformer +import java.lang.instrument.Instrumentation +import java.security.ProtectionDomain +import java.util.* + +/** + * Used to collect classes through instrumentation. + */ +class ClassRecorder { + val usedInstrumentedClasses = HashSet() + val instrumentedClasses = HashSet() + val scannedClasses = HashSet() +} + +/** + * Use global state to do the collection. + */ +val classRecorder = ClassRecorder() + +/** + * This is a hook called from each quasar getStack call, which happens on suspension. We construct a callstack and + * extract the part of the stack between the quasar scheduler and the getStack call, which should contain all methods/classes + * relevant to this suspension. + */ +fun recordUsedInstrumentedCallStack() { + val throwable = Throwable() + var index = 0 + while (true) { + require (index < throwable.stackTrace.size) { "Can't find getStack call" } + val stackElement = throwable.stackTrace[index] + if (stackElement.className == "co.paralleluniverse.fibers.Stack" && stackElement.methodName == "getStack") { + break + } + index++ + } + index++ + while (true) { + require (index < throwable.stackTrace.size) { "Can't find Fiber call" } + val stackElement = throwable.stackTrace[index] + if (stackElement.className.startsWith("co.paralleluniverse")) { + break + } + classRecorder.usedInstrumentedClasses.add(stackElement.className) + index++ + } +} + +/** + * This is a hook called from the method instrumentor visitor. Note that this should only be called once we're sure + * instrumentation will happen. + */ +fun recordInstrumentedClass(className: String) { + classRecorder.instrumentedClasses.add(className) +} + +/** + * This is a hook called from QuasarInstrumentor, after the exclude filtering, but before examining the bytecode. + */ +fun recordScannedClass(className: String?) { + if (className != null) { + classRecorder.scannedClasses.add(className) + } +} + +/** + * Arguments to this javaagent. + * + * @param truncate A comma-separated list of packages to trim from the exclude patterns. + * @param expand A comma-separated list of packages to expand in the glob output. This is useful for certain top-level + * domains that we don't want to completely exclude, because later on classes may be loaded from those namespaces + * that require instrumentation. + * @param separator The package part separator character used in the above lists. + */ +data class Arguments( + val truncate: List? = null, + val expand: List? = null, + val separator: Char = '.' +) + +/** + * This javaagent instruments quasar to extract information about what classes are scanned, instrumented, and used at + * runtime. On process exit the javaagent tries to calculate what an appropriate exclude pattern should be. + */ +class QuasarInstrumentationHookAgent { + companion object { + @JvmStatic + fun premain(argumentsString: String?, instrumentation: Instrumentation) { + + var arguments = Arguments() + argumentsString?.let { + it.split(";").forEach { + val (key, value) = it.split("=") + when (key) { + "truncate" -> arguments = arguments.copy(truncate = value.split(",")) + "expand" -> arguments = arguments.copy(expand = value.split(",")) + "separator" -> arguments = arguments.copy(separator = value.toCharArray()[0]) + } + } + } + + Runtime.getRuntime().addShutdownHook(Thread { + println("Instrumented classes: ${classRecorder.instrumentedClasses.size}") + classRecorder.instrumentedClasses.forEach { + println(" $it") + } + println("Used instrumented classes: ${classRecorder.usedInstrumentedClasses.size}") + classRecorder.usedInstrumentedClasses.forEach { + println(" $it") + } + println("Scanned classes: ${classRecorder.scannedClasses.size}") + classRecorder.scannedClasses.take(20).forEach { + println(" $it") + } + println(" (...)") + val scannedTree = PackageTree.fromStrings(classRecorder.scannedClasses.toList(), '/') + val instrumentedTree = PackageTree.fromStrings(classRecorder.instrumentedClasses.toList(), '/') + println("Suggested exclude globs:") + val truncate = arguments.truncate?.let { PackageTree.fromStrings(it, arguments.separator) } + // The separator append is a hack, it causes a package with an empty name to be added to the exclude tree, + // which practically causes that level of the tree to be always expanded in the output globs. + val expand = arguments.expand?.let { PackageTree.fromStrings(it.map { "$it${arguments.separator}" }, arguments.separator) } + val truncatedTree = truncate?.let { scannedTree.truncate(it)} ?: scannedTree + val expandedTree = expand?.let { instrumentedTree.merge(it) } ?: instrumentedTree + val globs = truncatedTree.toGlobs(expandedTree) + globs.forEach { + println(" $it") + } + println("Quasar exclude expression:") + println(" x(${globs.joinToString(";")})") + }) + instrumentation.addTransformer(QuasarInstrumentationHook) + } + } + +} + +object QuasarInstrumentationHook : ClassFileTransformer { + val classPool = ClassPool.getDefault() + + val hookClassName = "net.corda.quasarhook.QuasarInstrumentationHookKt" + + val instrumentMap = mapOf Unit>( + "co/paralleluniverse/fibers/Stack" to { clazz -> + // This is called on each suspend, we hook into it to get the stack trace of actually used Suspendables + val getStackMethod = clazz.methods.single { it.name == "getStack" } + getStackMethod.insertBefore( + "$hookClassName.${::recordUsedInstrumentedCallStack.name}();" + ) + }, + "co/paralleluniverse/fibers/instrument/InstrumentMethod" to { clazz -> + // This is called on each instrumented method + val acceptMethod = clazz.declaredMethods.single { it.name == "collectCodeBlocks" } + acceptMethod.insertBefore( + "$hookClassName.${::recordInstrumentedClass.name}(this.className);" + ) + }, + "co/paralleluniverse/fibers/instrument/QuasarInstrumentor" to { clazz -> + val instrumentClassMethods = clazz.methods.filter { + it.name == "instrumentClass" + } + // TODO this is very brittle, we want to match on a specific instrumentClass() function. We could use the function signature, but that may change between versions anyway. Why is this function overloaded?? + instrumentClassMethods[0].insertBefore( + "$hookClassName.${::recordScannedClass.name}(className);" + ) + } + ) + + override fun transform( + loader: ClassLoader?, + className: String, + classBeingRedefined: Class<*>?, + protectionDomain: ProtectionDomain?, + classfileBuffer: ByteArray + ): ByteArray { + return try { + val instrument = instrumentMap.get(className) + return instrument?.let { + val clazz = classPool.makeClass(ByteArrayInputStream(classfileBuffer)) + it(clazz) + clazz.toBytecode() + } ?: classfileBuffer + } catch (throwable: Throwable) { + println("SOMETHING WENT WRONG") + throwable.printStackTrace(System.out) + classfileBuffer + } + } +} + +data class Glob(val parts: List, val isFull: Boolean) { + override fun toString(): String { + if (isFull) { + return parts.joinToString(".") + } else { + return "${parts.joinToString(".")}**" + } + } +} + +/** + * Build up a tree from parts of the package names. + */ +data class PackageTree(val branches: Map) { + fun isEmpty() = branches.isEmpty() + + /** + * Merge the tree with [other]. + */ + fun merge(other: PackageTree): PackageTree { + val mergedBranches = HashMap(branches) + other.branches.forEach { (key, tree) -> + mergedBranches.compute(key) { _, previousTree -> + previousTree?.merge(tree) ?: tree + } + } + return PackageTree(mergedBranches) + } + + /** + * Truncate the tree below [other]. + */ + fun truncate(other: PackageTree): PackageTree { + if (other.isEmpty()) { + return empty + } else { + val truncatedBranches = HashMap(branches) + other.branches.forEach { (key, tree) -> + truncatedBranches.compute(key) { _, previousTree -> + previousTree?.truncate(tree) ?: empty + } + } + return PackageTree(truncatedBranches) + } + } + + companion object { + val empty = PackageTree(emptyMap()) + fun fromString(fullClassName: String, separator: Char): PackageTree { + var current = empty + fullClassName.split(separator).reversed().forEach { + current = PackageTree(mapOf(it to current)) + } + return current + } + + fun fromStrings(fullClassNames: List, separator: Char): PackageTree { + return mergeAll(fullClassNames.map { PackageTree.fromString(it, separator) }) + } + + fun mergeAll(trees: List): PackageTree { + return trees.foldRight(PackageTree.empty, PackageTree::merge) + } + } + + /** + * Construct minimal globs that match this tree but don't match [excludeTree]. + */ + fun toGlobs(excludeTree: PackageTree): List { + data class State( + val include: PackageTree, + val exclude: PackageTree, + val globSoFar: List + ) + val toExpandList = LinkedList(listOf(State(this, excludeTree, emptyList()))) + val globs = ArrayList() + while (true) { + val state = toExpandList.pollFirst() ?: break + if (state.exclude.branches.isEmpty()) { + globs.add(Glob(state.globSoFar, state.include.isEmpty())) + } else { + state.include.branches.forEach { (key, subTree) -> + val excludeSubTree = state.exclude.branches[key] + if (excludeSubTree != null) { + toExpandList.addLast(State(subTree, excludeSubTree, state.globSoFar + key)) + } else { + globs.add(Glob(state.globSoFar + key, subTree.isEmpty())) + } + } + } + } + return globs + } +} diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt index 24f05f7ff3..f86753bfba 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt @@ -2,16 +2,16 @@ package net.corda.contracts.universal import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import java.math.BigDecimal -import java.security.PublicKey import java.time.Instant val UNIVERSAL_PROGRAM_ID = UniversalContract() class UniversalContract : Contract { - data class State(override val participants: List, + data class State(override val participants: List, val details: Arrangement) : ContractState { override val contract = UNIVERSAL_PROGRAM_ID } @@ -316,7 +316,7 @@ class UniversalContract : Contract { override val legalContractReference: SecureHash get() = throw UnsupportedOperationException() - fun generateIssue(tx: TransactionBuilder, arrangement: Arrangement, at: PartyAndReference, notary: PublicKey) { + fun generateIssue(tx: TransactionBuilder, arrangement: Arrangement, at: PartyAndReference, notary: Party) { check(tx.inputStates().isEmpty()) tx.addOutputState(State(listOf(notary), arrangement)) tx.addCommand(Commands.Issue(), at.party.owningKey) diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt index cbb2a488fd..5d275c2876 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt @@ -130,15 +130,15 @@ class Cap { val paymentFinal = arrange { highStreetBank.owes(acmeCorp, 250.K, EUR) } - val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractInitial) + val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) - val stateAfterFixingFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractAfterFixingFirst) + val stateAfterFixingFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterFixingFirst) - val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractAfterExecutionFirst) - val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), paymentFirst) + val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterExecutionFirst) + val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFirst) - val stateAfterFixingFinal = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractAfterFixingFinal) - val statePaymentFinal = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), paymentFinal) + val stateAfterFixingFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterFixingFinal) + val statePaymentFinal = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFinal) val contractLimitedCap = arrange { rollOut("2016-04-01".ld, "2017-04-01".ld, Frequency.SemiAnnual, object { diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt index 634d1964a6..0c69005b22 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt @@ -44,11 +44,11 @@ class Caplet { val contractFinal = arrange { highStreetBank.owes(acmeCorp, 250.K, EUR) } - val stateStart = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contract) + val stateStart = UniversalContract.State(listOf(DUMMY_NOTARY), contract) - val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractFixed) + val stateFixed = UniversalContract.State(listOf(DUMMY_NOTARY), contractFixed) - val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractFinal) + val stateFinal = UniversalContract.State(listOf(DUMMY_NOTARY), contractFinal) @Test fun issue() { diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt index 4265468ac2..9bea6c3c37 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt @@ -43,9 +43,9 @@ class FXFwdTimeOption val TEST_TX_TIME_BEFORE_MATURITY: Instant get() = Instant.parse("2018-05-01T12:00:00.00Z") val TEST_TX_TIME_AFTER_MATURITY: Instant get() = Instant.parse("2018-06-02T12:00:00.00Z") - val inState = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), initialContract) - val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), outContract1) - val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), outContract2) + val inState = UniversalContract.State(listOf(DUMMY_NOTARY), initialContract) + val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract1) + val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), outContract2) @Test fun `issue - signature`() { diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt index 38495f2e06..272ae30171 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt @@ -25,18 +25,18 @@ class FXSwap { val transfer1 = arrange { highStreetBank.owes(acmeCorp, 1070.K, EUR) } val transfer2 = arrange { acmeCorp.owes(highStreetBank, 1.M, USD) } - val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transfer1) - val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transfer2) + val outState1 = UniversalContract.State(listOf(DUMMY_NOTARY), transfer1) + val outState2 = UniversalContract.State(listOf(DUMMY_NOTARY), transfer2) val transferBad1 = arrange { highStreetBank.owes(acmeCorp, 1070.K, USD) } // wrong currency val transferBad2 = arrange { acmeCorp.owes(highStreetBank, 900.K, USD) } // wrong amount val transferBad3 = arrange { highStreetBank.owes(highStreetBank, 1070.K, EUR) } // wrong party - val outStateBad1 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transferBad1) - val outStateBad2 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transferBad2) - val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transferBad3) + val outStateBad1 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad1) + val outStateBad2 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad2) + val outStateBad3 = UniversalContract.State(listOf(DUMMY_NOTARY), transferBad3) - val inState = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contract) + val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract) @Test fun `issue - signature`() { diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt index 252f70ca70..e80cffc391 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt @@ -122,12 +122,12 @@ class IRS { val paymentFirst = arrange { highStreetBank.owes(acmeCorp, 250.K, EUR) } - val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractInitial) + val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) - val stateAfterFixingFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractAfterFixingFirst) - val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractAfterExecutionFirst) + val stateAfterFixingFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterFixingFirst) + val stateAfterExecutionFirst = UniversalContract.State(listOf(DUMMY_NOTARY), contractAfterExecutionFirst) - val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), paymentFirst) + val statePaymentFirst = UniversalContract.State(listOf(DUMMY_NOTARY), paymentFirst) @Test diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt index 13d09b9f7a..c91a78be11 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt @@ -36,7 +36,7 @@ class RollOutTests { } } } - val stateStart = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contract) + val stateStart = UniversalContract.State(listOf(DUMMY_NOTARY), contract) val contractStep1a = arrange { rollOut("2016-10-03".ld, "2017-09-01".ld, Frequency.Monthly) { @@ -55,8 +55,8 @@ class RollOutTests { highStreetBank.owes(acmeCorp, 10.K, USD) } - val stateStep1a = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractStep1a) - val stateStep1b = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractStep1b) + val stateStep1a = UniversalContract.State(listOf(DUMMY_NOTARY), contractStep1a) + val stateStep1b = UniversalContract.State(listOf(DUMMY_NOTARY), contractStep1b) val contract_transfer1 = arrange { highStreetBank.owes(acmeCorp, 10.K, USD) diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt index e1f04921a4..7dea92bc4f 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt @@ -54,7 +54,7 @@ class Swaption { } - val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractInitial) + val stateInitial = UniversalContract.State(listOf(DUMMY_NOTARY), contractInitial) @Test fun issue() { diff --git a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt index 6f45ea0742..458bacb4bb 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt @@ -33,12 +33,12 @@ class ZeroCouponBond { val transfer = arrange { highStreetBank.owes(acmeCorp, 100.K, GBP) } val transferWrong = arrange { highStreetBank.owes(acmeCorp, 80.K, GBP) } - val inState = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contract) + val inState = UniversalContract.State(listOf(DUMMY_NOTARY), contract) - val outState = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transfer) - val outStateWrong = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), transferWrong) + val outState = UniversalContract.State(listOf(DUMMY_NOTARY), transfer) + val outStateWrong = UniversalContract.State(listOf(DUMMY_NOTARY), transferWrong) - val outStateMove = UniversalContract.State(listOf(DUMMY_NOTARY.owningKey), contractMove) + val outStateMove = UniversalContract.State(listOf(DUMMY_NOTARY), contractMove) @Test fun basic() { diff --git a/finance/isolated/src/main/kotlin/net/corda/contracts/AnotherDummyContract.kt b/finance/isolated/src/main/kotlin/net/corda/contracts/AnotherDummyContract.kt index d61a903a38..ae94bab519 100644 --- a/finance/isolated/src/main/kotlin/net/corda/contracts/AnotherDummyContract.kt +++ b/finance/isolated/src/main/kotlin/net/corda/contracts/AnotherDummyContract.kt @@ -3,6 +3,7 @@ package net.corda.contracts.isolated import net.corda.core.contracts.* import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -13,7 +14,7 @@ val ANOTHER_DUMMY_PROGRAM_ID = AnotherDummyContract() class AnotherDummyContract : Contract, net.corda.core.node.DummyContractBackdoor { data class State(val magicNumber: Int = 0) : ContractState { override val contract = ANOTHER_DUMMY_PROGRAM_ID - override val participants: List + override val participants: List get() = emptyList() } diff --git a/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java b/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java index a907811919..0923897593 100644 --- a/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java +++ b/finance/src/main/java/net/corda/contracts/ICommercialPaperState.java @@ -1,6 +1,7 @@ package net.corda.contracts; import net.corda.core.contracts.*; +import net.corda.core.identity.AbstractParty; import java.security.PublicKey; import java.time.*; @@ -12,7 +13,7 @@ import java.util.*; * ultimately either language can be used against a common test framework (and therefore can be used for real). */ public interface ICommercialPaperState extends ContractState { - ICommercialPaperState withOwner(PublicKey newOwner); + ICommercialPaperState withOwner(AbstractParty newOwner); ICommercialPaperState withFaceValue(Amount> newFaceValue); diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index a38251e24d..d2bb7eb3f8 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -9,6 +9,9 @@ import net.corda.core.contracts.Contract; import net.corda.core.contracts.TransactionForContract.*; import net.corda.core.contracts.clauses.*; import net.corda.core.crypto.*; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.AbstractParty; +import net.corda.core.identity.AnonymousParty; import net.corda.core.identity.Party; import net.corda.core.node.services.*; import net.corda.core.transactions.*; @@ -34,14 +37,14 @@ public class JavaCommercialPaper implements Contract { @SuppressWarnings("unused") public static class State implements OwnableState, ICommercialPaperState { private PartyAndReference issuance; - private PublicKey owner; + private AbstractParty owner; private Amount> faceValue; private Instant maturityDate; public State() { } // For serialization - public State(PartyAndReference issuance, PublicKey owner, Amount> faceValue, + public State(PartyAndReference issuance, AbstractParty owner, Amount> faceValue, Instant maturityDate) { this.issuance = issuance; this.owner = owner; @@ -53,13 +56,13 @@ public class JavaCommercialPaper implements Contract { return new State(this.issuance, this.owner, this.faceValue, this.maturityDate); } - public ICommercialPaperState withOwner(PublicKey newOwner) { + public ICommercialPaperState withOwner(AbstractParty newOwner) { return new State(this.issuance, newOwner, this.faceValue, this.maturityDate); } @NotNull @Override - public Pair withNewOwner(@NotNull PublicKey newOwner) { + public Pair withNewOwner(@NotNull AbstractParty newOwner) { return new Pair<>(new Commands.Move(), new State(this.issuance, newOwner, this.faceValue, this.maturityDate)); } @@ -76,7 +79,7 @@ public class JavaCommercialPaper implements Contract { } @NotNull - public PublicKey getOwner() { + public AbstractParty getOwner() { return owner; } @@ -117,12 +120,12 @@ public class JavaCommercialPaper implements Contract { } public State withoutOwner() { - return new State(issuance, NullPublicKey.INSTANCE, faceValue, maturityDate); + return new State(issuance, new AnonymousParty(NullPublicKey.INSTANCE), faceValue, maturityDate); } @NotNull @Override - public List getParticipants() { + public List getParticipants() { return ImmutableList.of(this.owner); } } @@ -162,12 +165,12 @@ public class JavaCommercialPaper implements Contract { @NotNull List inputs, @NotNull List outputs, @NotNull List> commands, - @NotNull State groupingKey) { + State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Move.class); // There should be only a single input due to aggregation above State input = single(inputs); - if (!cmd.getSigners().contains(input.getOwner())) + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); // Check the output CP state is the same as the input state, ignoring the owner field. @@ -194,13 +197,13 @@ public class JavaCommercialPaper implements Contract { @NotNull List inputs, @NotNull List outputs, @NotNull List> commands, - @NotNull State groupingKey) { + State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Redeem.class); // There should be only a single input due to aggregation above State input = single(inputs); - if (!cmd.getSigners().contains(input.getOwner())) + if (!cmd.getSigners().contains(input.getOwner().getOwningKey())) throw new IllegalStateException("Failed requirement: the transaction is signed by the owner of the CP"); Timestamp timestamp = tx.getTimestamp(); @@ -237,7 +240,7 @@ public class JavaCommercialPaper implements Contract { @NotNull List inputs, @NotNull List outputs, @NotNull List> commands, - @NotNull State groupingKey) { + State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); State output = single(outputs); Timestamp timestampCommand = tx.getTimestamp(); @@ -304,7 +307,7 @@ public class JavaCommercialPaper implements Contract { } public TransactionBuilder generateIssue(@NotNull PartyAndReference issuance, @NotNull Amount> faceValue, @Nullable Instant maturityDate, @NotNull Party notary, Integer encumbrance) { - State state = new State(issuance, issuance.getParty().getOwningKey(), faceValue, maturityDate); + State state = new State(issuance, issuance.getParty(), faceValue, maturityDate); TransactionState output = new TransactionState<>(state, notary, encumbrance); return new TransactionType.General.Builder(notary).withItems(output, new Command(new Commands.Issue(), issuance.getParty().getOwningKey())); } @@ -317,12 +320,12 @@ public class JavaCommercialPaper implements Contract { public void generateRedeem(TransactionBuilder tx, StateAndRef paper, VaultService vault) throws InsufficientBalanceException { vault.generateSpend(tx, StructuresKt.withoutIssuer(paper.getState().getData().getFaceValue()), paper.getState().getData().getOwner(), null); tx.addInputState(paper); - tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner())); + tx.addCommand(new Command(new Commands.Redeem(), paper.getState().getData().getOwner().getOwningKey())); } - public void generateMove(TransactionBuilder tx, StateAndRef paper, PublicKey newOwner) { + public void generateMove(TransactionBuilder tx, StateAndRef paper, AbstractParty newOwner) { tx.addInputState(paper); tx.addOutputState(new TransactionState<>(new State(paper.getState().getData().getIssuance(), newOwner, paper.getState().getData().getFaceValue(), paper.getState().getData().getMaturityDate()), paper.getState().getNotary(), paper.getState().getEncumbrance())); - tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner())); + tx.addCommand(new Command(new Commands.Move(), paper.getState().getData().getOwner().getOwningKey())); } } diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 5719c292d2..38c637ea1a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -10,6 +10,7 @@ import net.corda.core.contracts.clauses.GroupClauseVerifier import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService import net.corda.core.random63BitValue @@ -19,7 +20,6 @@ import net.corda.core.schemas.QueryableState import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji import net.corda.schemas.CommercialPaperSchemaV1 -import java.security.PublicKey import java.time.Instant import java.util.* @@ -61,22 +61,22 @@ class CommercialPaper : Contract { data class State( val issuance: PartyAndReference, - override val owner: PublicKey, + override val owner: AbstractParty, val faceValue: Amount>, val maturityDate: Instant ) : OwnableState, QueryableState, ICommercialPaperState { override val contract = CP_PROGRAM_ID - override val participants: List + override val participants: List get() = listOf(owner) val token: Issued get() = Issued(issuance, Terms(faceValue.token, maturityDate)) - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)" // Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later, - override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner) + override fun withOwner(newOwner: AbstractParty): ICommercialPaperState = copy(owner = newOwner) override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) @@ -91,7 +91,7 @@ class CommercialPaper : Contract { is CommercialPaperSchemaV1 -> CommercialPaperSchemaV1.PersistentCommercialPaperState( issuanceParty = this.issuance.party.owningKey.toBase58String(), issuanceRef = this.issuance.reference.bytes, - owner = this.owner.toBase58String(), + owner = this.owner.owningKey.toBase58String(), maturity = this.maturityDate, faceValue = this.faceValue.quantity, currency = this.faceValue.token.product.currencyCode, @@ -146,7 +146,7 @@ class CommercialPaper : Contract { val command = commands.requireSingleCommand() val input = inputs.single() requireThat { - "the transaction is signed by the owner of the CP" using (input.owner in command.signers) + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) "the state is propagated" using (outputs.size == 1) // Don't need to check anything else, as if outputs.size == 1 then the output is equal to // the input ignoring the owner field due to the grouping. @@ -175,7 +175,7 @@ class CommercialPaper : Contract { "the paper must have matured" using (time >= input.maturityDate) "the received amount equals the face value" using (received == input.faceValue) "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner in command.signers) + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) } return setOf(command.value) @@ -196,17 +196,17 @@ class CommercialPaper : Contract { * at the moment: this restriction is not fundamental and may be lifted later. */ fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { - val state = TransactionState(State(issuance, issuance.party.owningKey, faceValue, maturityDate), notary) + val state = TransactionState(State(issuance, issuance.party, faceValue, maturityDate), notary) return TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } /** * Updates the given partial transaction with an input/output/command to reassign ownership of the paper. */ - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { tx.addInputState(paper) tx.addOutputState(TransactionState(paper.state.data.copy(owner = newOwner), paper.state.notary)) - tx.addCommand(Commands.Move(), paper.state.data.owner) + tx.addCommand(Commands.Move(), paper.state.data.owner.owningKey) } /** @@ -223,12 +223,12 @@ class CommercialPaper : Contract { val amount = paper.state.data.faceValue.let { amount -> Amount(amount.quantity, amount.token.product) } vault.generateSpend(tx, amount, paper.state.data.owner) tx.addInputState(paper) - tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner) + tx.addCommand(CommercialPaper.Commands.Redeem(), paper.state.data.owner.owningKey) } } -infix fun CommercialPaper.State.`owned by`(owner: PublicKey) = copy(owner = owner) +infix fun CommercialPaper.State.`owned by`(owner: AbstractParty) = copy(owner = owner) infix fun CommercialPaper.State.`with notary`(notary: Party) = TransactionState(this, notary) -infix fun ICommercialPaperState.`owned by`(newOwner: PublicKey) = withOwner(newOwner) +infix fun ICommercialPaperState.`owned by`(newOwner: AbstractParty) = withOwner(newOwner) diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index f46d777b1d..7be5b921f4 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -3,13 +3,13 @@ package net.corda.contracts import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.NullPublicKey +import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.Emoji -import java.security.PublicKey import java.time.Instant import java.util.* @@ -27,19 +27,19 @@ class CommercialPaperLegacy : Contract { data class State( val issuance: PartyAndReference, - override val owner: PublicKey, + override val owner: AbstractParty, val faceValue: Amount>, val maturityDate: Instant ) : OwnableState, ICommercialPaperState { override val contract = CP_LEGACY_PROGRAM_ID override val participants = listOf(owner) - fun withoutOwner() = copy(owner = NullPublicKey) - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + fun withoutOwner() = copy(owner = NULL_PARTY) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) override fun toString() = "${Emoji.newspaper}CommercialPaper(of $faceValue redeemable on $maturityDate by '$issuance', owned by $owner)" // Although kotlin is smart enough not to need these, as we are using the ICommercialPaperState, we need to declare them explicitly for use later, - override fun withOwner(newOwner: PublicKey): ICommercialPaperState = copy(owner = newOwner) + override fun withOwner(newOwner: AbstractParty): ICommercialPaperState = copy(owner = newOwner) override fun withFaceValue(newFaceValue: Amount>): ICommercialPaperState = copy(faceValue = newFaceValue) override fun withMaturityDate(newMaturityDate: Instant): ICommercialPaperState = copy(maturityDate = newMaturityDate) @@ -70,7 +70,7 @@ class CommercialPaperLegacy : Contract { is Commands.Move -> { val input = inputs.single() requireThat { - "the transaction is signed by the owner of the CP" using (input.owner in command.signers) + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) "the state is propagated" using (outputs.size == 1) // Don't need to check anything else, as if outputs.size == 1 then the output is equal to // the input ignoring the owner field due to the grouping. @@ -86,7 +86,7 @@ class CommercialPaperLegacy : Contract { "the paper must have matured" using (time >= input.maturityDate) "the received amount equals the face value" using (received == input.faceValue) "the paper must be destroyed" using outputs.isEmpty() - "the transaction is signed by the owner of the CP" using (input.owner in command.signers) + "the transaction is signed by the owner of the CP" using (input.owner.owningKey in command.signers) } } @@ -114,14 +114,14 @@ class CommercialPaperLegacy : Contract { fun generateIssue(issuance: PartyAndReference, faceValue: Amount>, maturityDate: Instant, notary: Party): TransactionBuilder { - val state = State(issuance, issuance.party.owningKey, faceValue, maturityDate) + val state = State(issuance, issuance.party, faceValue, maturityDate) return TransactionBuilder(notary = notary).withItems(state, Command(Commands.Issue(), issuance.party.owningKey)) } - fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: PublicKey) { + fun generateMove(tx: TransactionBuilder, paper: StateAndRef, newOwner: AbstractParty) { tx.addInputState(paper) tx.addOutputState(paper.state.data.withOwner(newOwner)) - tx.addCommand(Command(Commands.Move(), paper.state.data.owner)) + tx.addCommand(Command(Commands.Move(), paper.state.data.owner.owningKey)) } @Throws(InsufficientBalanceException::class) @@ -130,6 +130,6 @@ class CommercialPaperLegacy : Contract { // Add the cash movement using the states in our vault. vault.generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner) tx.addInputState(paper) - tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner)) + tx.addCommand(Command(Commands.Redeem(), paper.state.data.owner.owningKey)) } } diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt index c964e78421..9e273dc09a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -21,7 +21,6 @@ import net.corda.core.utilities.Emoji import net.corda.schemas.CashSchemaV1 import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger -import java.security.PublicKey import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -88,27 +87,27 @@ class Cash : OnLedgerAsset() { override val amount: Amount>, /** There must be a MoveCommand signed by this key to claim the amount. */ - override val owner: PublicKey + override val owner: AbstractParty ) : FungibleAsset, QueryableState { - constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) + constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) - override val exitKeys = setOf(owner, amount.token.issuer.party.owningKey) + override val exitKeys = setOf(owner.owningKey, amount.token.issuer.party.owningKey) override val contract = CASH_PROGRAM_ID override val participants = listOf(owner) - override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) override fun toString() = "${Emoji.bagOfCash}Cash($amount at ${amount.token.issuer} owned by $owner)" - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) /** Object Relational Mapping support. */ override fun generateMappedObject(schema: MappedSchema): PersistentState { return when (schema) { is CashSchemaV1 -> CashSchemaV1.PersistentCashState( - owner = this.owner.toBase58String(), + owner = this.owner.owningKey.toBase58String(), pennies = this.amount.quantity, currency = this.amount.token.product.currencyCode, issuerParty = this.amount.token.issuer.party.owningKey.toBase58String(), @@ -149,16 +148,16 @@ class Cash : OnLedgerAsset() { /** * Puts together an issuance transaction from the given template, that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: PublicKey, notary: Party) + fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: AbstractParty, notary: Party) = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) - override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) @@ -176,7 +175,7 @@ class Cash : OnLedgerAsset() { * if there are none, or if any of the cash states cannot be added together (i.e. are * different currencies or issuers). */ -fun Iterable.sumCashBy(owner: PublicKey): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() +fun Iterable.sumCashBy(owner: AbstractParty): Amount> = filterIsInstance().filter { it.owner == owner }.map { it.amount }.sumOrThrow() /** * Sums the cash states in the list, throwing an exception if there are none, or if any of the cash @@ -192,12 +191,12 @@ fun Iterable.sumCashOrZero(currency: Issued): Amount().map { it.amount }.sumOrZero(currency) } -fun Cash.State.ownedBy(owner: PublicKey) = copy(owner = owner) -fun Cash.State.issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party.toAnonymous())))) +fun Cash.State.ownedBy(owner: AbstractParty) = copy(owner = owner) +fun Cash.State.issuedBy(party: AbstractParty) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = amount.token.issuer.copy(party = party)))) fun Cash.State.issuedBy(deposit: PartyAndReference) = copy(amount = Amount(amount.quantity, amount.token.copy(issuer = deposit))) fun Cash.State.withDeposit(deposit: PartyAndReference): Cash.State = copy(amount = amount.copy(token = amount.token.copy(issuer = deposit))) -infix fun Cash.State.`owned by`(owner: PublicKey) = ownedBy(owner) +infix fun Cash.State.`owned by`(owner: AbstractParty) = ownedBy(owner) infix fun Cash.State.`issued by`(party: AbstractParty) = issuedBy(party) infix fun Cash.State.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = withDeposit(deposit) @@ -209,6 +208,6 @@ val DUMMY_CASH_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ val DUMMY_CASH_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=UK"), DUMMY_CASH_ISSUER_KEY.public).ref(1) } /** An extension property that lets you write 100.DOLLARS.CASH */ -val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NullPublicKey) -/** An extension property that lets you get a cash state from an issued token, under the [NullPublicKey] */ -val Amount>.STATE: Cash.State get() = Cash.State(this, NullPublicKey) +val Amount.CASH: Cash.State get() = Cash.State(Amount(quantity, Issued(DUMMY_CASH_ISSUER, token)), NULL_PARTY) +/** An extension property that lets you get a cash state from an issued token, under the [NULL_PARTY] */ +val Amount>.STATE: Cash.State get() = Cash.State(this, NULL_PARTY) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt index e47dd127c4..c8cadda2ef 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -9,10 +9,10 @@ import net.corda.core.contracts.clauses.GroupClauseVerifier import net.corda.core.contracts.clauses.verifyClause import net.corda.core.crypto.SecureHash import net.corda.core.crypto.newSecureRandom +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder -import java.security.PublicKey import java.util.* ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -94,21 +94,21 @@ class CommodityContract : OnLedgerAsset>, /** There must be a MoveCommand signed by this key to claim the amount */ - override val owner: PublicKey + override val owner: AbstractParty ) : FungibleAsset { - constructor(deposit: PartyAndReference, amount: Amount, owner: PublicKey) + constructor(deposit: PartyAndReference, amount: Amount, owner: AbstractParty) : this(Amount(amount.quantity, Issued(deposit, amount.token)), owner) override val contract = COMMODITY_PROGRAM_ID - override val exitKeys = Collections.singleton(owner) + override val exitKeys = Collections.singleton(owner.owningKey) override val participants = listOf(owner) - override fun move(newAmount: Amount>, newOwner: PublicKey): FungibleAsset + override fun move(newAmount: Amount>, newOwner: AbstractParty): FungibleAsset = copy(amount = amount.copy(newAmount.quantity), owner = newOwner) override fun toString() = "Commodity($amount at ${amount.token.issuer} owned by $owner)" - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner)) } // Just for grouping @@ -145,17 +145,17 @@ class CommodityContract : OnLedgerAsset, pennies: Long, owner: PublicKey, notary: Party) + fun generateIssue(tx: TransactionBuilder, tokenDef: Issued, pennies: Long, owner: AbstractParty, notary: Party) = generateIssue(tx, Amount(pennies, tokenDef), owner, notary) /** * Puts together an issuance transaction for the specified amount that starts out being owned by the given pubkey. */ - fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: PublicKey, notary: Party) + fun generateIssue(tx: TransactionBuilder, amount: Amount>, owner: AbstractParty, notary: Party) = generateIssue(tx, TransactionState(State(amount, owner), notary), generateIssueCommand()) - override fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + override fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) override fun generateExitCommand(amount: Amount>) = Commands.Exit(amount) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt index ba154e2225..9cb6508363 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -5,7 +5,9 @@ import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.contracts.clause.* import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* -import net.corda.core.crypto.* +import net.corda.core.crypto.NULL_PARTY +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party @@ -22,6 +24,36 @@ import java.security.PublicKey import java.time.Duration import java.time.Instant import java.util.* +import kotlin.collections.Collection +import kotlin.collections.Iterable +import kotlin.collections.List +import kotlin.collections.Map +import kotlin.collections.Set +import kotlin.collections.all +import kotlin.collections.asIterable +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.contains +import kotlin.collections.distinct +import kotlin.collections.emptySet +import kotlin.collections.filter +import kotlin.collections.filterIsInstance +import kotlin.collections.first +import kotlin.collections.firstOrNull +import kotlin.collections.forEach +import kotlin.collections.groupBy +import kotlin.collections.isNotEmpty +import kotlin.collections.iterator +import kotlin.collections.listOf +import kotlin.collections.map +import kotlin.collections.none +import kotlin.collections.reduce +import kotlin.collections.set +import kotlin.collections.setOf +import kotlin.collections.single +import kotlin.collections.toSet +import kotlin.collections.union +import kotlin.collections.withIndex // Just a fake program identifier for now. In a real system it could be, for instance, the hash of the program bytecode. val OBLIGATION_PROGRAM_ID = Obligation() @@ -274,27 +306,20 @@ class Obligation

: Contract { data class State

( var lifecycle: Lifecycle = Lifecycle.NORMAL, /** Where the debt originates from (obligor) */ - val obligor: AnonymousParty, + val obligor: AbstractParty, val template: Terms

, val quantity: Long, /** The public key of the entity the contract pays to */ - val beneficiary: PublicKey + val beneficiary: AbstractParty ) : FungibleAsset>, NettableState, MultilateralNetState

> { - constructor(lifecycle: Lifecycle = Lifecycle.NORMAL, - obligor: Party, - template: Terms

, - quantity: Long, - beneficiary: PublicKey) - : this(lifecycle, obligor.toAnonymous(), template, quantity, beneficiary) - override val amount: Amount>> = Amount(quantity, Issued(obligor.ref(0), template)) override val contract = OBLIGATION_PROGRAM_ID - override val exitKeys: Collection = setOf(beneficiary) + override val exitKeys: Collection = setOf(beneficiary.owningKey) val dueBefore: Instant = template.dueBefore - override val participants: List = listOf(obligor.owningKey, beneficiary) - override val owner: PublicKey = beneficiary + override val participants: List = listOf(obligor, beneficiary) + override val owner: AbstractParty = beneficiary - override fun move(newAmount: Amount>>, newOwner: PublicKey): State

+ override fun move(newAmount: Amount>>, newOwner: AbstractParty): State

= copy(quantity = newAmount.quantity, beneficiary = newOwner) override fun toString() = when (lifecycle) { @@ -305,7 +330,7 @@ class Obligation

: Contract { override val bilateralNetState: BilateralNetState

get() { check(lifecycle == Lifecycle.NORMAL) - return BilateralNetState(setOf(obligor.owningKey, beneficiary), template) + return BilateralNetState(setOf(obligor, beneficiary), template) } override val multilateralNetState: MultilateralNetState

get() { @@ -327,7 +352,7 @@ class Obligation

: Contract { } } - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(beneficiary = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(beneficiary = newOwner)) } // Just for grouping @@ -418,7 +443,7 @@ class Obligation

: Contract { } } } - val owningPubKeys = inputs.filter { it is State

}.map { (it as State

).beneficiary }.toSet() + val owningPubKeys = inputs.filter { it is State

}.map { (it as State

).beneficiary.owningKey }.toSet() val keysThatSigned = setLifecycleCommand.signers.toSet() requireThat { "the owning keys are a subset of the signing keys" using keysThatSigned.containsAll(owningPubKeys) @@ -433,7 +458,7 @@ class Obligation

: Contract { * and same parties involved). */ fun generateCloseOutNetting(tx: TransactionBuilder, - signer: PublicKey, + signer: AbstractParty, vararg states: State

) { val netState = states.firstOrNull()?.bilateralNetState @@ -447,7 +472,7 @@ class Obligation

: Contract { val out = states.reduce(State

::net) if (out.quantity > 0L) tx.addOutputState(out) - tx.addCommand(Commands.Net(NetType.PAYMENT), signer) + tx.addCommand(Commands.Net(NetType.PAYMENT), signer.owningKey) } /** @@ -475,9 +500,9 @@ class Obligation

: Contract { obligor: AbstractParty, issuanceDef: Terms

, pennies: Long, - beneficiary: PublicKey, + beneficiary: AbstractParty, notary: Party) - = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor.toAnonymous(), issuanceDef, pennies, beneficiary), notary), Commands.Issue()) + = OnLedgerAsset.generateIssue(tx, TransactionState(State(Lifecycle.NORMAL, obligor, issuanceDef, pennies, beneficiary), notary), Commands.Issue()) fun generatePaymentNetting(tx: TransactionBuilder, issued: Issued>, @@ -487,8 +512,8 @@ class Obligation

: Contract { "all states are in the normal lifecycle state " using (states.all { it.lifecycle == Lifecycle.NORMAL }) } val groups = states.groupBy { it.multilateralNetState } - val partyLookup = HashMap() - val signers = states.map { it.beneficiary }.union(states.map { it.obligor.owningKey }).toSet() + val partyLookup = HashMap() + val signers = states.map { it.beneficiary }.union(states.map { it.obligor }).toSet() // Create a lookup table of the party that each public key represents. states.map { it.obligor }.forEach { partyLookup.put(it.owningKey, it) } @@ -502,12 +527,12 @@ class Obligation

: Contract { netBalances // Convert the balances into obligation state objects .map { entry -> - State(Lifecycle.NORMAL, partyLookup[entry.key.first]!!, + State(Lifecycle.NORMAL, entry.key.first, netState.template, entry.value.quantity, entry.key.second) } // Add the new states to the TX .forEach { tx.addOutputState(it, notary) } - tx.addCommand(Commands.Net(NetType.PAYMENT), signers.toList()) + tx.addCommand(Commands.Net(NetType.PAYMENT), signers.map { it.owningKey }) } } @@ -533,14 +558,14 @@ class Obligation

: Contract { // Produce a new set of states val groups = statesAndRefs.groupBy { it.state.data.amount.token } for ((_, stateAndRefs) in groups) { - val partiesUsed = ArrayList() + val partiesUsed = ArrayList() stateAndRefs.forEach { stateAndRef -> val outState = stateAndRef.state.data.copy(lifecycle = lifecycle) tx.addInputState(stateAndRef) tx.addOutputState(outState, notary) partiesUsed.add(stateAndRef.state.data.beneficiary) } - tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.distinct()) + tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) } tx.setTime(issuanceDef.dueBefore, issuanceDef.timeTolerance) } @@ -578,7 +603,7 @@ class Obligation

: Contract { val template: Terms

= issuanceDef.product val obligationTotal: Amount

= Amount(states.map { it.data }.sumObligations

().quantity, template.product) var obligationRemaining: Amount

= obligationTotal - val assetSigners = HashSet() + val assetSigners = HashSet() statesAndRefs.forEach { tx.addInputState(it) } @@ -611,7 +636,7 @@ class Obligation

: Contract { } // Add the asset move command and obligation settle - tx.addCommand(moveCommand, assetSigners.toList()) + tx.addCommand(moveCommand, assetSigners.map { it.owningKey }) tx.addCommand(Commands.Settle(Amount((obligationTotal - obligationRemaining).quantity, issuanceDef)), obligationIssuer.owningKey) } @@ -630,11 +655,11 @@ class Obligation

: Contract { * * @return a map of obligor/beneficiary pairs to the balance due. */ -fun

extractAmountsDue(product: Obligation.Terms

, states: Iterable>): Map, Amount>> { - val balances = HashMap, Amount>>() +fun

extractAmountsDue(product: Obligation.Terms

, states: Iterable>): Map, Amount>> { + val balances = HashMap, Amount>>() states.forEach { state -> - val key = Pair(state.obligor.owningKey, state.beneficiary) + val key = Pair(state.obligor, state.beneficiary) val balance = balances[key] ?: Amount(0L, product) balances[key] = balance + Amount(state.amount.quantity, state.amount.token.product) } @@ -645,8 +670,8 @@ fun

extractAmountsDue(product: Obligation.Terms

, states: Iterable netAmountsDue(balances: Map, Amount

>): Map, Amount

> { - val nettedBalances = HashMap, Amount

>() +fun netAmountsDue(balances: Map, Amount>): Map, Amount> { + val nettedBalances = HashMap, Amount>() balances.forEach { balance -> val (obligor, beneficiary) = balance.key @@ -669,9 +694,11 @@ fun

netAmountsDue(balances: Map, Amount

> * * @param balances payments due, indexed by obligor and beneficiary. Zero balances are stripped from the map before being * returned. + * @param P type of party to operate on. + * @param T token that balances represent */ -fun

sumAmountsDue(balances: Map, Amount

>): Map { - val sum = HashMap() +fun sumAmountsDue(balances: Map, Amount>): Map { + val sum = HashMap() // Fill the map with zeroes initially balances.keys.forEach { @@ -712,11 +739,11 @@ fun

Iterable.sumObligationsOrZero(issuanceDef: Issued>().filter { it.lifecycle == Obligation.Lifecycle.NORMAL }.map { it.amount }.sumOrZero(issuanceDef) infix fun Obligation.State.at(dueBefore: Instant) = copy(template = template.copy(dueBefore = dueBefore)) -infix fun Obligation.State.between(parties: Pair) = copy(obligor = parties.first.toAnonymous(), beneficiary = parties.second) -infix fun Obligation.State.`owned by`(owner: PublicKey) = copy(beneficiary = owner) -infix fun Obligation.State.`issued by`(party: AbstractParty) = copy(obligor = party.toAnonymous()) +infix fun Obligation.State.between(parties: Pair) = copy(obligor = parties.first, beneficiary = parties.second) +infix fun Obligation.State.`owned by`(owner: AbstractParty) = copy(beneficiary = owner) +infix fun Obligation.State.`issued by`(party: AbstractParty) = copy(obligor = party) // For Java users: -@Suppress("unused") fun Obligation.State.ownedBy(owner: PublicKey) = copy(beneficiary = owner) +@Suppress("unused") fun Obligation.State.ownedBy(owner: AbstractParty) = copy(beneficiary = owner) @Suppress("unused") fun Obligation.State.issuedBy(party: AnonymousParty) = copy(obligor = party) @@ -728,4 +755,4 @@ val DUMMY_OBLIGATION_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,O val Issued.OBLIGATION_DEF: Obligation.Terms get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) val Amount>.OBLIGATION: Obligation.State - get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER.toAnonymous(), token.OBLIGATION_DEF, quantity, NullPublicKey) + get() = Obligation.State(Obligation.Lifecycle.NORMAL, DUMMY_OBLIGATION_ISSUER, token.OBLIGATION_DEF, quantity, NULL_PARTY) diff --git a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt index 47f286dece..175182d48a 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -1,7 +1,7 @@ package net.corda.contracts.asset -import net.corda.contracts.clause.AbstractConserveAmount import net.corda.core.contracts.* +import net.corda.core.identity.AbstractParty import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace @@ -51,9 +51,9 @@ abstract class OnLedgerAsset> : C @JvmStatic fun , T: Any> generateSpend(tx: TransactionBuilder, amount: Amount, - to: PublicKey, + to: AbstractParty, acceptableStates: List>, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData): Pair> { // Discussion // @@ -90,7 +90,7 @@ abstract class OnLedgerAsset> : C } else { null } - val keysUsed = gathered.map { it.state.data.owner } + val keysUsed = gathered.map { it.state.data.owner.owningKey } val states = gathered.groupBy { it.state.data.amount.token.issuer }.map { val coins = it.value @@ -165,7 +165,7 @@ abstract class OnLedgerAsset> : C @JvmStatic fun , T: Any> generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData, generateExitCommand: (Amount>) -> CommandData): PublicKey { val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) @@ -193,7 +193,7 @@ abstract class OnLedgerAsset> : C for (state in gathered) tx.addInputState(state) for (state in outputs) tx.addOutputState(state) - tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner }) + tx.addCommand(generateMoveCommand(), gathered.map { it.state.data.owner.owningKey }) tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys }) return amountIssued.token.issuer.party.owningKey } @@ -250,5 +250,5 @@ abstract class OnLedgerAsset> : C * implementations to have fields in their state which we don't know about here, and we simply leave them untouched * when sending out "change" from spending/exiting. */ - abstract fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey): TransactionState + abstract fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty): TransactionState } diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt index dde9354f9d..c48386cd1e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt @@ -3,6 +3,7 @@ package net.corda.contracts.clause import net.corda.contracts.asset.OnLedgerAsset import net.corda.core.contracts.* import net.corda.core.contracts.clauses.Clause +import net.corda.core.identity.AbstractParty import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.loggerFor import java.security.PublicKey @@ -31,7 +32,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : @Throws(InsufficientBalanceException::class) fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, assetStates: List>, - deriveState: (TransactionState, Amount>, PublicKey) -> TransactionState, + deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData, generateExitCommand: (Amount>) -> CommandData): PublicKey = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand) diff --git a/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt b/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt index 452dded632..5eee72380e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt @@ -6,6 +6,7 @@ import net.corda.contracts.asset.extractAmountsDue import net.corda.contracts.asset.sumAmountsDue import net.corda.core.contracts.* import net.corda.core.contracts.clauses.Clause +import net.corda.core.identity.AbstractParty import java.security.PublicKey /** @@ -22,7 +23,7 @@ interface NetState

{ * Bilateral states are used in close-out netting. */ data class BilateralNetState

( - val partyKeys: Set, + val partyKeys: Set, override val template: Obligation.Terms

) : NetState

( - var fixedRatePayer: P, + open class FixedLeg( + var fixedRatePayer: AbstractParty, notional: Amount, paymentFrequency: Frequency, effectiveDate: LocalDate, @@ -335,7 +336,7 @@ class InterestRateSwap : Contract { if (other?.javaClass != javaClass) return false if (!super.equals(other)) return false - other as FixedLeg<*> + other as FixedLeg if (fixedRatePayer != other.fixedRatePayer) return false if (fixedRate != other.fixedRate) return false @@ -347,7 +348,7 @@ class InterestRateSwap : Contract { override fun hashCode() = super.hashCode() + 31 * Objects.hash(fixedRatePayer, fixedRate, rollConvention) // Can't autogenerate as not a data class :-( - fun copy(fixedRatePayer: P = this.fixedRatePayer, + fun copy(fixedRatePayer: AbstractParty = this.fixedRatePayer, notional: Amount = this.notional, paymentFrequency: Frequency = this.paymentFrequency, effectiveDate: LocalDate = this.effectiveDate, @@ -365,17 +366,11 @@ class InterestRateSwap : Contract { fixedRatePayer, notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment, dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, fixedRate, rollConvention) - - fun toAnonymous(): FixedLeg { - return FixedLeg(fixedRatePayer.toAnonymous(), notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment, - dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, - fixedRate, rollConvention) - } } @CordaSerializable - open class FloatingLeg

( - var floatingRatePayer: P, + open class FloatingLeg( + var floatingRatePayer: AbstractParty, notional: Amount, paymentFrequency: Frequency, effectiveDate: LocalDate, @@ -411,7 +406,7 @@ class InterestRateSwap : Contract { if (other?.javaClass != javaClass) return false if (!super.equals(other)) return false - other as FloatingLeg<*> + other as FloatingLeg if (floatingRatePayer != other.floatingRatePayer) return false if (rollConvention != other.rollConvention) return false @@ -433,7 +428,7 @@ class InterestRateSwap : Contract { index, indexSource, indexTenor) - fun copy(floatingRatePayer: P = this.floatingRatePayer, + fun copy(floatingRatePayer: AbstractParty = this.floatingRatePayer, notional: Amount = this.notional, paymentFrequency: Frequency = this.paymentFrequency, effectiveDate: LocalDate = this.effectiveDate, @@ -462,13 +457,6 @@ class InterestRateSwap : Contract { paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, rollConvention, fixingRollConvention, resetDayInMonth, fixingPeriod, resetRule, fixingsPerPayment, fixingCalendar, index, indexSource, indexTenor) - - fun toAnonymous(): FloatingLeg { - return FloatingLeg(floatingRatePayer.toAnonymous(), notional, paymentFrequency, effectiveDate, effectiveDateAdjustment, terminationDate, terminationDateAdjustment, - dayCountBasisDay, dayCountBasisYear, dayInMonth, paymentRule, paymentDelay, paymentCalendar, interestPeriodAdjustment, - rollConvention, fixingRollConvention, resetDayInMonth, fixingPeriodOffset, resetRule, fixingsPerPayment, - fixingCalendar, index, indexSource, indexTenor) - } } override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) @@ -478,7 +466,7 @@ class InterestRateSwap : Contract { * Common superclass for IRS contract clauses, which defines behaviour on match/no-match, and provides * helper functions for the clauses. */ - abstract class AbstractIRSClause : Clause, Commands, UniqueIdentifier>() { + abstract class AbstractIRSClause : Clause() { // These functions may make more sense to use for basket types, but for now let's leave them here fun checkLegDates(legs: List) { requireThat { @@ -494,7 +482,7 @@ class InterestRateSwap : Contract { "The notional for all legs must be the same" using legs.all { it.notional == legs[0].notional } } for (leg: CommonLeg in legs) { - if (leg is FixedLeg<*>) { + if (leg is FixedLeg) { requireThat { // TODO: Confirm: would someone really enter a swap with a negative fixed rate? "Fixed leg rate must be positive" using leg.fixedRate.isPositive() @@ -520,9 +508,9 @@ class InterestRateSwap : Contract { } } - class Group : GroupClauseVerifier, Commands, UniqueIdentifier>(AnyOf(Agree(), Fix(), Pay(), Mature())) { + class Group : GroupClauseVerifier(AnyOf(Agree(), Fix(), Pay(), Mature())) { // Group by Trade ID for in / out states - override fun groupStates(tx: TransactionForContract): List, UniqueIdentifier>> { + override fun groupStates(tx: TransactionForContract): List> { return tx.groupStates { state -> state.linearId } } } @@ -543,12 +531,12 @@ class InterestRateSwap : Contract { override val requiredCommands: Set> = setOf(Commands.Agree::class.java) override fun verify(tx: TransactionForContract, - inputs: List>, - outputs: List>, + inputs: List, + outputs: List, commands: List>, groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance>().single() + val irs = outputs.filterIsInstance().single() requireThat { "There are no in states for an agreement" using inputs.isEmpty() "There are events in the fix schedule" using (irs.calculation.fixedLegPaymentSchedule.isNotEmpty()) @@ -579,13 +567,13 @@ class InterestRateSwap : Contract { override val requiredCommands: Set> = setOf(Commands.Refix::class.java) override fun verify(tx: TransactionForContract, - inputs: List>, - outputs: List>, + inputs: List, + outputs: List, commands: List>, groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() - val irs = outputs.filterIsInstance>().single() - val prevIrs = inputs.filterIsInstance>().single() + val irs = outputs.filterIsInstance().single() + val prevIrs = inputs.filterIsInstance().single() val paymentDifferences = getFloatingLegPaymentsDifferences(prevIrs.calculation.floatingLegPaymentSchedule, irs.calculation.floatingLegPaymentSchedule) // Having both of these tests are "redundant" as far as verify() goes, however, by performing both @@ -624,8 +612,8 @@ class InterestRateSwap : Contract { override val requiredCommands: Set> = setOf(Commands.Pay::class.java) override fun verify(tx: TransactionForContract, - inputs: List>, - outputs: List>, + inputs: List, + outputs: List, commands: List>, groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() @@ -640,12 +628,12 @@ class InterestRateSwap : Contract { override val requiredCommands: Set> = setOf(Commands.Mature::class.java) override fun verify(tx: TransactionForContract, - inputs: List>, - outputs: List>, + inputs: List, + outputs: List, commands: List>, groupingKey: UniqueIdentifier?): Set { val command = tx.commands.requireSingleCommand() - val irs = inputs.filterIsInstance>().single() + val irs = inputs.filterIsInstance().single() requireThat { "No more fixings to be applied" using (irs.calculation.nextFixingDate() == null) "The irs is fully consumed and there is no id matched output state" using outputs.isEmpty() @@ -667,9 +655,9 @@ class InterestRateSwap : Contract { /** * The state class contains the 4 major data classes. */ - data class State

( - val fixedLeg: FixedLeg

, - val floatingLeg: FloatingLeg

, + data class State( + val fixedLeg: FixedLeg, + val floatingLeg: FloatingLeg, val calculation: Calculation, val common: Common, override val linearId: UniqueIdentifier = UniqueIdentifier(common.tradeID) @@ -682,15 +670,15 @@ class InterestRateSwap : Contract { override val ref = common.tradeID - override val participants: List - get() = parties.map { it.owningKey } + override val participants: List + get() = parties override fun isRelevant(ourKeys: Set): Boolean { return fixedLeg.fixedRatePayer.owningKey.containsAny(ourKeys) || floatingLeg.floatingRatePayer.owningKey.containsAny(ourKeys) } - override val parties: List - get() = listOf(fixedLeg.fixedRatePayer.toAnonymous(), floatingLeg.floatingRatePayer.toAnonymous()) + override val parties: List + get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { val nextFixingOf = nextFixingOf() ?: return null @@ -700,10 +688,10 @@ class InterestRateSwap : Contract { return ScheduledActivity(flowLogicRefFactory.create(FixingFlow.FixingRoleDecider::class.java, thisStateRef), instant) } - override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg.toAnonymous(), fixedLeg.toAnonymous(), calculation, common, notary) + override fun generateAgreement(notary: Party): TransactionBuilder = InterestRateSwap().generateAgreement(floatingLeg, fixedLeg, calculation, common, notary) override fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) { - InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this.toAnonymous(), oldState.state.notary), oldState.ref), fix) + InterestRateSwap().generateFix(ptx, StateAndRef(TransactionState(this, oldState.state.notary), oldState.ref), fix) } override fun nextFixingOf(): FixOf? { @@ -737,22 +725,13 @@ class InterestRateSwap : Contract { * Just makes printing it out a bit better for those who don't have 80000 column wide monitors. */ fun prettyPrint() = toString().replace(",", "\n") - - fun toAnonymous(): State { - return if (this.fixedLeg.fixedRatePayer is AnonymousParty) { - @Suppress("UNCHECKED_CAST") - this as State - } else { - State(fixedLeg.toAnonymous(), floatingLeg.toAnonymous(), calculation, common, linearId) - } - } } /** * This generates the agreement state and also the schedules from the initial data. * Note: The day count, interest rate calculation etc are not finished yet, but they are demonstrable. */ - fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation, + fun generateAgreement(floatingLeg: FloatingLeg, fixedLeg: FixedLeg, calculation: Calculation, common: Common, notary: Party): TransactionBuilder { val fixedLegPaymentSchedule = LinkedHashMap() @@ -816,7 +795,7 @@ class InterestRateSwap : Contract { } } - fun generateFix(tx: TransactionBuilder, irs: StateAndRef>, fixing: Fix) { + fun generateFix(tx: TransactionBuilder, irs: StateAndRef, fixing: Fix) { tx.addInputState(irs) val fixedRate = FixedRate(RatioUnit(fixing.value)) tx.addOutputState( diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSExport.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSExport.kt index 68887f88d4..3caa9e21c2 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSExport.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSExport.kt @@ -1,6 +1,6 @@ package net.corda.irs.contract -fun InterestRateSwap.State<*>.exportIRSToCSV(): String = +fun InterestRateSwap.State.exportIRSToCSV(): String = "Fixed Leg\n" + FixedRatePaymentEvent.CSVHeader + "\n" + this.calculation.fixedLegPaymentSchedule.toSortedMap().values.map { it.asCSV() }.joinToString("\n") + "\n" + "Floating Leg\n" + FloatingRatePaymentEvent.CSVHeader + "\n" + diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt index 402a4f89c8..7e7520c65a 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/AutoOfferFlow.kt @@ -2,9 +2,10 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.DealState -import net.corda.core.identity.AbstractParty import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AbstractParty import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.SingletonSerializeAsToken @@ -37,6 +38,7 @@ object AutoOfferFlow { } @InitiatingFlow + @StartableByRPC class Requester(val dealToBeOffered: DealState) : FlowLogic() { companion object { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt index e962181798..045e56230c 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/FixingFlow.kt @@ -3,22 +3,22 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.TransientProperty import net.corda.core.contracts.* -import net.corda.core.identity.Party -import net.corda.core.crypto.keys import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.SchedulableFlow +import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.ServiceType import net.corda.core.seconds import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.trace import net.corda.flows.TwoPartyDealFlow import java.math.BigDecimal -import java.security.KeyPair import java.security.PublicKey object FixingFlow { @@ -113,15 +113,18 @@ object FixingFlow { StateAndRef(state, payload.ref) } - override val myKeyPair: KeyPair get() { - val myPublicKey = serviceHub.myInfo.legalIdentity.owningKey - val myKeys = dealToFix.state.data.parties.single { it.owningKey == myPublicKey }.owningKey.keys - return serviceHub.keyManagementService.toKeyPair(myKeys) + override val myKey: PublicKey get() { + dealToFix.state.data.parties.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } + return serviceHub.legalIdentityKey } override val notaryNode: NodeInfo get() { return serviceHub.networkMapCache.notaryNodes.single { it.notaryIdentity == dealToFix.state.notary } } + + @Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat { + // Add some constraints here. + } } @@ -136,6 +139,7 @@ object FixingFlow { * Fixer role is chosen, then that will be initiated by the [FixingSession] message sent from the other party. */ @InitiatingFlow + @SchedulableFlow class FixingRoleDecider(val ref: StateRef, override val progressTracker: ProgressTracker) : FlowLogic() { @Suppress("unused") // Used via reflection. constructor(ref: StateRef) : this(ref, tracker()) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index 47812e4ba8..fd6d7c79cf 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -4,6 +4,7 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub @@ -44,6 +45,7 @@ object UpdateBusinessDayFlow { @InitiatingFlow + @StartableByRPC class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic() { constructor(date: LocalDate) : this(date, tracker()) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt index fc607ef272..343ebccb79 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/plugin/IRSPlugin.kt @@ -1,15 +1,9 @@ package net.corda.irs.plugin -import net.corda.core.contracts.StateRef import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.irs.api.InterestRateSwapAPI -import net.corda.irs.contract.InterestRateSwap -import net.corda.irs.flows.AutoOfferFlow import net.corda.irs.flows.FixingFlow -import net.corda.irs.flows.UpdateBusinessDayFlow -import java.time.Duration -import java.time.LocalDate import java.util.function.Function class IRSPlugin : CordaPluginRegistry() { @@ -18,9 +12,4 @@ class IRSPlugin : CordaPluginRegistry() { "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() ) override val servicePlugins = listOf(Function(FixingFlow::Service)) - override val requiredFlows: Map> = mapOf( - AutoOfferFlow.Requester::class.java.name to setOf(InterestRateSwap.State::class.java.name), - UpdateBusinessDayFlow.Broadcast::class.java.name to setOf(LocalDate::class.java.name), - FixingFlow.FixingRoleDecider::class.java.name to setOf(StateRef::class.java.name, Duration::class.java.name), - FixingFlow.Floater::class.java.name to setOf(Party::class.java.name, FixingFlow.FixingSession::class.java.name)) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/simulation/IRSSimulation.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt similarity index 87% rename from samples/irs-demo/src/main/kotlin/net/corda/simulation/IRSSimulation.kt rename to samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt index 7ae55b7a83..45b2f9a4d0 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/simulation/IRSSimulation.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt @@ -1,6 +1,7 @@ -package net.corda.simulation +package net.corda.irs.simulation import co.paralleluniverse.fibers.Suspendable +import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.readValue import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures @@ -9,12 +10,11 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.RunOnCallerThread import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party import net.corda.core.flatMap import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party import net.corda.core.map import net.corda.core.node.services.linearHeadsOfType import net.corda.core.success @@ -24,11 +24,11 @@ import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.Instigator import net.corda.irs.contract.InterestRateSwap import net.corda.jackson.JacksonSupport +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.utilities.transaction import net.corda.testing.initiateSingleShotFlow import net.corda.testing.node.InMemoryMessagingNetwork -import net.corda.testing.node.MockIdentityService -import java.security.KeyPair +import java.security.PublicKey import java.time.LocalDate import java.util.* @@ -37,7 +37,7 @@ import java.util.* * 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) { - val om = JacksonSupport.createInMemoryMapper(MockIdentityService(network.identities)) + lateinit var om: ObjectMapper init { currentDateAndTime = LocalDate.of(2016, 3, 8).atStartOfDay() @@ -47,6 +47,7 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten override fun startMainSimulation(): ListenableFuture { val future = SettableFuture.create() + om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentity })) startIRSDealBetween(0, 1).success { // Next iteration is a pause. @@ -79,9 +80,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten return future } - private fun loadLinearHeads(node: SimulatedNode): Map>> { + private fun loadLinearHeads(node: SimulatedNode): Map> { return node.database.transaction { - node.services.vaultService.linearHeadsOfType>() + node.services.vaultService.linearHeadsOfType() } } @@ -90,8 +91,8 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten val node1: SimulatedNode = banks[i] val node2: SimulatedNode = banks[j] - val swaps: Map>> = loadLinearHeads(node1) - val theDealRef: StateAndRef> = swaps.values.single() + val swaps: Map> = loadLinearHeads(node1) + val theDealRef: StateAndRef = swaps.values.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 @@ -121,16 +122,16 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten // 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 irs = om.readValue>(javaClass.classLoader.getResource("simulation/trade.json")) - irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity.toAnonymous() - irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity.toAnonymous() + val irs = om.readValue(javaClass.classLoader.getResource("net/corda/irs/simulation/trade.json")) + irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity + irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity @InitiatingFlow class StartDealFlow(val otherParty: Party, val payload: AutoOffer, - val myKeyPair: KeyPair) : FlowLogic() { + val myKey: PublicKey) : FlowLogic() { @Suspendable - override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKeyPair)) + override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey)) } @Suppress("UNCHECKED_CAST") diff --git a/samples/irs-demo/src/main/kotlin/net/corda/simulation/Simulation.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt similarity index 94% rename from samples/irs-demo/src/main/kotlin/net/corda/simulation/Simulation.kt rename to samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt index 1fefb0c1a9..6f770662c2 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/simulation/Simulation.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt @@ -1,8 +1,7 @@ -package net.corda.simulation +package net.corda.irs.simulation import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.crypto.location import net.corda.core.flatMap import net.corda.core.flows.FlowLogic import net.corda.core.messaging.SingleMessageRecipient @@ -10,7 +9,6 @@ import net.corda.core.node.CityDatabase import net.corda.core.node.PhysicalLocation import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.containsType -import net.corda.core.then import net.corda.core.utilities.DUMMY_MAP import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_REGULATOR @@ -128,7 +126,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun start(): MockNetwork.MockNode { super.start() - javaClass.classLoader.getResourceAsStream("example.rates.txt").use { + javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use { database.transaction { findService().upload(it) } @@ -203,7 +201,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, * A place for simulations to stash human meaningful text about what the node is "thinking", which might appear * in the UI somewhere. */ - val extraNodeLabels = Collections.synchronizedMap(HashMap()) + val extraNodeLabels: MutableMap = Collections.synchronizedMap(HashMap()) /** * Iterates the simulation by one step. @@ -216,7 +214,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, * @return the message that was processed, or null if no node accepted a message in this round. */ open fun iterate(): InMemoryMessagingNetwork.MessageTransfer? { - if (networkSendManuallyPumped) { network.messagingNetwork.pumpSend(false) } @@ -288,19 +285,4 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun stop() { network.stopNodes() } - - /** - * Given a function that returns a future, iterates that function with arguments like (0, 1), (1, 2), (2, 3) etc - * each time the returned future completes. - */ - fun startTradingCircle(tradeBetween: (indexA: Int, indexB: Int) -> ListenableFuture<*>) { - fun next(i: Int, j: Int) { - tradeBetween(i, j).then { - val ni = (i + 1) % banks.size - val nj = (j + 1) % banks.size - next(ni, nj) - } - } - next(0, 1) - } } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/HttpUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/HttpUtils.kt index 3bc03a01ed..1d6687d4f5 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/HttpUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/HttpUtils.kt @@ -26,7 +26,7 @@ fun postJson(url: URL, data: String): Boolean { fun uploadFile(url: URL, file: String): Boolean { val body = MultipartBody.Builder() .setType(MultipartBody.FORM) - .addFormDataPart("rates", "example.rates.txt", RequestBody.create(MediaType.parse("text/plain"), file)) + .addFormDataPart("rates", "net/corda/irs/simulation/example.rates.txt", RequestBody.create(MediaType.parse("text/plain"), file)) .build() return makeRequest(Request.Builder().url(url).post(body).build()) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/simulation/TradeSimulation.kt b/samples/irs-demo/src/main/kotlin/net/corda/simulation/TradeSimulation.kt index 477ce1eb94..e69de29bb2 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/simulation/TradeSimulation.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/simulation/TradeSimulation.kt @@ -1,76 +0,0 @@ -package net.corda.simulation - -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import net.corda.contracts.CommercialPaper -import net.corda.contracts.asset.DUMMY_CASH_ISSUER -import net.corda.contracts.testing.fillWithSomeTestCash -import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.OwnableState -import net.corda.core.contracts.`issued by` -import net.corda.core.days -import net.corda.core.flatMap -import net.corda.core.flows.FlowStateMachine -import net.corda.core.seconds -import net.corda.core.transactions.SignedTransaction -import net.corda.flows.TwoPartyTradeFlow.Buyer -import net.corda.flows.TwoPartyTradeFlow.Seller -import net.corda.testing.initiateSingleShotFlow -import net.corda.testing.node.InMemoryMessagingNetwork -import java.time.Instant - -/** - * Simulates a never ending series of trades that go pair-wise through the banks (e.g. A and B trade with each other, - * then B and C trade with each other, then C and A etc). - */ -class TradeSimulation(runAsync: Boolean, latencyInjector: InMemoryMessagingNetwork.LatencyCalculator?) : Simulation(false, runAsync, latencyInjector) { - override fun startMainSimulation(): ListenableFuture { - startTradingCircle { i, j -> tradeBetween(i, j) } - return Futures.immediateFailedFuture(UnsupportedOperationException("This future never completes")) - } - - private fun tradeBetween(buyerBankIndex: Int, sellerBankIndex: Int): ListenableFuture> { - val buyer = banks[buyerBankIndex] - val seller = banks[sellerBankIndex] - - buyer.services.fillWithSomeTestCash(1500.DOLLARS, notary.info.notaryIdentity) - - val issuance = run { - val tx = CommercialPaper().generateIssue( - seller.info.legalIdentity.ref(1, 2, 3), - 1100.DOLLARS `issued by` DUMMY_CASH_ISSUER, - Instant.now() + 10.days, - notary.info.notaryIdentity) - tx.setTime(Instant.now(), 30.seconds) - val notaryKey = notary.services.notaryIdentityKey - val sellerKey = seller.services.legalIdentityKey - tx.signWith(notaryKey) - tx.signWith(sellerKey) - tx.toSignedTransaction(true) - } - seller.services.recordTransactions(issuance) - - val amount = 1000.DOLLARS - - @Suppress("UNCHECKED_CAST") - val buyerFuture = buyer.initiateSingleShotFlow(Seller::class) { - Buyer(it, notary.info.notaryIdentity, amount, CommercialPaper.State::class.java) - }.flatMap { (it.stateMachine as FlowStateMachine).resultFuture } - - val sellerKey = seller.services.legalIdentityKey - val sellerFlow = Seller( - buyer.info.legalIdentity, - notary.info, - issuance.tx.outRef(0), - amount, - sellerKey) - - showConsensusFor(listOf(buyer, seller, notary)) - showProgressFor(listOf(buyer, seller)) - - val sellerFuture = seller.services.startFlow(sellerFlow).resultFuture - - return Futures.successfulAsList(buyerFuture, sellerFuture) - } - -} diff --git a/samples/irs-demo/src/main/resources/example-irs-trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json similarity index 100% rename from samples/irs-demo/src/main/resources/example-irs-trade.json rename to samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json diff --git a/samples/irs-demo/src/main/resources/example.rates.txt b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example.rates.txt similarity index 100% rename from samples/irs-demo/src/main/resources/example.rates.txt rename to samples/irs-demo/src/main/resources/net/corda/irs/simulation/example.rates.txt diff --git a/samples/irs-demo/src/main/resources/simulation/trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json similarity index 100% rename from samples/irs-demo/src/main/resources/simulation/trade.json rename to samples/irs-demo/src/main/resources/net/corda/irs/simulation/trade.json diff --git a/samples/irs-demo/src/main/resources/simulation/example.rates.txt b/samples/irs-demo/src/main/resources/simulation/example.rates.txt deleted file mode 100644 index 2b9893f806..0000000000 --- a/samples/irs-demo/src/main/resources/simulation/example.rates.txt +++ /dev/null @@ -1,228 +0,0 @@ -# Some pretend noddy rate fixes, for the interest rate oracles. - -ICE LIBOR 2016-03-16 1M = 0.678 -ICE LIBOR 2016-03-16 2M = 0.655 -EURIBOR 2016-03-15 1M = 0.123 -EURIBOR 2016-03-15 2M = 0.111 - -# Previous fixings -ICE LIBOR 2016-03-07 3M = 0.0063516 -ICE LIBOR 2016-03-07 3M = 0.0063516 -ICE LIBOR 2016-03-08 3M = 0.0063517 -ICE LIBOR 2016-03-09 3M = 0.0063518 -ICE LIBOR 2016-03-10 3M = 0.0063519 -ICE LIBOR 2016-06-06 3M = 0.0063520 -ICE LIBOR 2016-06-07 3M = 0.0063521 -ICE LIBOR 2016-06-08 3M = 0.0063522 -ICE LIBOR 2016-06-09 3M = 0.0063523 -ICE LIBOR 2016-06-10 3M = 0.0063524 -ICE LIBOR 2016-09-06 3M = 0.0063525 -ICE LIBOR 2016-09-07 3M = 0.0063526 -ICE LIBOR 2016-09-08 3M = 0.0063527 -ICE LIBOR 2016-09-09 3M = 0.0063528 -ICE LIBOR 2016-09-10 3M = 0.0063529 -ICE LIBOR 2016-12-06 3M = 0.0063530 -ICE LIBOR 2016-12-07 3M = 0.0063531 -ICE LIBOR 2016-12-08 3M = 0.0063532 -ICE LIBOR 2016-12-09 3M = 0.0063533 -ICE LIBOR 2016-12-10 3M = 0.0063534 -ICE LIBOR 2017-03-06 3M = 0.0063535 -ICE LIBOR 2017-03-07 3M = 0.0063536 -ICE LIBOR 2017-03-08 3M = 0.0063537 -ICE LIBOR 2017-03-09 3M = 0.0063538 -ICE LIBOR 2017-03-10 3M = 0.0063539 -ICE LIBOR 2017-06-06 3M = 0.0063540 -ICE LIBOR 2017-06-07 3M = 0.0063541 -ICE LIBOR 2017-06-08 3M = 0.0063542 -ICE LIBOR 2017-06-09 3M = 0.0063543 -ICE LIBOR 2017-06-10 3M = 0.0063544 -ICE LIBOR 2017-09-06 3M = 0.0063545 -ICE LIBOR 2017-09-07 3M = 0.0063546 -ICE LIBOR 2017-09-08 3M = 0.0063547 -ICE LIBOR 2017-09-09 3M = 0.0063548 -ICE LIBOR 2017-09-10 3M = 0.0063549 -ICE LIBOR 2017-12-06 3M = 0.0063550 -ICE LIBOR 2017-12-07 3M = 0.0063551 -ICE LIBOR 2017-12-08 3M = 0.0063552 -ICE LIBOR 2017-12-09 3M = 0.0063553 -ICE LIBOR 2017-12-10 3M = 0.0063554 -ICE LIBOR 2018-03-06 3M = 0.0063555 -ICE LIBOR 2018-03-07 3M = 0.0063556 -ICE LIBOR 2018-03-08 3M = 0.0063557 -ICE LIBOR 2018-03-09 3M = 0.0063558 -ICE LIBOR 2018-03-10 3M = 0.0063559 -ICE LIBOR 2018-06-06 3M = 0.0063560 -ICE LIBOR 2018-06-07 3M = 0.0063561 -ICE LIBOR 2018-06-08 3M = 0.0063562 -ICE LIBOR 2018-06-09 3M = 0.0063563 -ICE LIBOR 2018-06-10 3M = 0.0063564 -ICE LIBOR 2018-09-06 3M = 0.0063565 -ICE LIBOR 2018-09-07 3M = 0.0063566 -ICE LIBOR 2018-09-08 3M = 0.0063567 -ICE LIBOR 2018-09-09 3M = 0.0063568 -ICE LIBOR 2018-09-10 3M = 0.0063569 -ICE LIBOR 2018-12-06 3M = 0.0063570 -ICE LIBOR 2018-12-07 3M = 0.0063571 -ICE LIBOR 2018-12-08 3M = 0.0063572 -ICE LIBOR 2018-12-09 3M = 0.0063573 -ICE LIBOR 2018-12-10 3M = 0.0063574 -ICE LIBOR 2019-03-06 3M = 0.0063575 -ICE LIBOR 2019-03-07 3M = 0.0063576 -ICE LIBOR 2019-03-08 3M = 0.0063577 -ICE LIBOR 2019-03-09 3M = 0.0063578 -ICE LIBOR 2019-03-10 3M = 0.0063579 -ICE LIBOR 2019-06-06 3M = 0.0063580 -ICE LIBOR 2019-06-07 3M = 0.0063581 -ICE LIBOR 2019-06-08 3M = 0.0063582 -ICE LIBOR 2019-06-09 3M = 0.0063583 -ICE LIBOR 2019-06-10 3M = 0.0063584 -ICE LIBOR 2019-09-06 3M = 0.0063585 -ICE LIBOR 2019-09-07 3M = 0.0063586 -ICE LIBOR 2019-09-08 3M = 0.0063587 -ICE LIBOR 2019-09-09 3M = 0.0063588 -ICE LIBOR 2019-09-10 3M = 0.0063589 -ICE LIBOR 2019-12-06 3M = 0.0063590 -ICE LIBOR 2019-12-07 3M = 0.0063591 -ICE LIBOR 2019-12-08 3M = 0.0063592 -ICE LIBOR 2019-12-09 3M = 0.0063593 -ICE LIBOR 2019-12-10 3M = 0.0063594 -ICE LIBOR 2020-03-06 3M = 0.0063595 -ICE LIBOR 2020-03-07 3M = 0.0063596 -ICE LIBOR 2020-03-08 3M = 0.0063597 -ICE LIBOR 2020-03-09 3M = 0.0063598 -ICE LIBOR 2020-03-10 3M = 0.0063599 -ICE LIBOR 2020-06-06 3M = 0.0063600 -ICE LIBOR 2020-06-07 3M = 0.0063601 -ICE LIBOR 2020-06-08 3M = 0.0063602 -ICE LIBOR 2020-06-09 3M = 0.0063603 -ICE LIBOR 2020-06-10 3M = 0.0063604 -ICE LIBOR 2020-09-06 3M = 0.0063605 -ICE LIBOR 2020-09-07 3M = 0.0063606 -ICE LIBOR 2020-09-08 3M = 0.0063607 -ICE LIBOR 2020-09-09 3M = 0.0063608 -ICE LIBOR 2020-09-10 3M = 0.0063609 -ICE LIBOR 2020-12-06 3M = 0.0063610 -ICE LIBOR 2020-12-07 3M = 0.0063611 -ICE LIBOR 2020-12-08 3M = 0.0063612 -ICE LIBOR 2020-12-09 3M = 0.0063613 -ICE LIBOR 2020-12-10 3M = 0.0063614 -ICE LIBOR 2021-03-06 3M = 0.0063615 -ICE LIBOR 2021-03-07 3M = 0.0063616 -ICE LIBOR 2021-03-08 3M = 0.0063617 -ICE LIBOR 2021-03-09 3M = 0.0063618 -ICE LIBOR 2021-03-10 3M = 0.0063619 -ICE LIBOR 2021-06-06 3M = 0.0063620 -ICE LIBOR 2021-06-07 3M = 0.0063621 -ICE LIBOR 2021-06-08 3M = 0.0063622 -ICE LIBOR 2021-06-09 3M = 0.0063623 -ICE LIBOR 2021-06-10 3M = 0.0063624 -ICE LIBOR 2021-09-06 3M = 0.0063625 -ICE LIBOR 2021-09-07 3M = 0.0063626 -ICE LIBOR 2021-09-08 3M = 0.0063627 -ICE LIBOR 2021-09-09 3M = 0.0063628 -ICE LIBOR 2021-09-10 3M = 0.0063629 -ICE LIBOR 2021-12-06 3M = 0.0063630 -ICE LIBOR 2021-12-07 3M = 0.0063631 -ICE LIBOR 2021-12-08 3M = 0.0063632 -ICE LIBOR 2021-12-09 3M = 0.0063633 -ICE LIBOR 2021-12-10 3M = 0.0063634 -ICE LIBOR 2022-03-06 3M = 0.0063635 -ICE LIBOR 2022-03-07 3M = 0.0063636 -ICE LIBOR 2022-03-08 3M = 0.0063637 -ICE LIBOR 2022-03-09 3M = 0.0063638 -ICE LIBOR 2022-03-10 3M = 0.0063639 -ICE LIBOR 2022-06-06 3M = 0.0063640 -ICE LIBOR 2022-06-07 3M = 0.0063641 -ICE LIBOR 2022-06-08 3M = 0.0063642 -ICE LIBOR 2022-06-09 3M = 0.0063643 -ICE LIBOR 2022-06-10 3M = 0.0063644 -ICE LIBOR 2022-09-06 3M = 0.0063645 -ICE LIBOR 2022-09-07 3M = 0.0063646 -ICE LIBOR 2022-09-08 3M = 0.0063647 -ICE LIBOR 2022-09-09 3M = 0.0063648 -ICE LIBOR 2022-09-10 3M = 0.0063649 -ICE LIBOR 2022-12-06 3M = 0.0063650 -ICE LIBOR 2022-12-07 3M = 0.0063651 -ICE LIBOR 2022-12-08 3M = 0.0063652 -ICE LIBOR 2022-12-09 3M = 0.0063653 -ICE LIBOR 2022-12-10 3M = 0.0063654 -ICE LIBOR 2023-03-06 3M = 0.0063655 -ICE LIBOR 2023-03-07 3M = 0.0063656 -ICE LIBOR 2023-03-08 3M = 0.0063657 -ICE LIBOR 2023-03-09 3M = 0.0063658 -ICE LIBOR 2023-03-10 3M = 0.0063659 -ICE LIBOR 2023-06-06 3M = 0.0063660 -ICE LIBOR 2023-06-07 3M = 0.0063661 -ICE LIBOR 2023-06-08 3M = 0.0063662 -ICE LIBOR 2023-06-09 3M = 0.0063663 -ICE LIBOR 2023-06-10 3M = 0.0063664 -ICE LIBOR 2023-09-06 3M = 0.0063665 -ICE LIBOR 2023-09-07 3M = 0.0063666 -ICE LIBOR 2023-09-08 3M = 0.0063667 -ICE LIBOR 2023-09-09 3M = 0.0063668 -ICE LIBOR 2023-09-10 3M = 0.0063669 -ICE LIBOR 2023-12-06 3M = 0.0063670 -ICE LIBOR 2023-12-07 3M = 0.0063671 -ICE LIBOR 2023-12-08 3M = 0.0063672 -ICE LIBOR 2023-12-09 3M = 0.0063673 -ICE LIBOR 2023-12-10 3M = 0.0063674 -ICE LIBOR 2024-03-06 3M = 0.0063675 -ICE LIBOR 2024-03-07 3M = 0.0063676 -ICE LIBOR 2024-03-08 3M = 0.0063677 -ICE LIBOR 2024-03-09 3M = 0.0063678 -ICE LIBOR 2024-03-10 3M = 0.0063679 -ICE LIBOR 2024-06-06 3M = 0.0063680 -ICE LIBOR 2024-06-07 3M = 0.0063681 -ICE LIBOR 2024-06-08 3M = 0.0063682 -ICE LIBOR 2024-06-09 3M = 0.0063683 -ICE LIBOR 2024-06-10 3M = 0.0063684 -ICE LIBOR 2024-09-06 3M = 0.0063685 -ICE LIBOR 2024-09-07 3M = 0.0063686 -ICE LIBOR 2024-09-08 3M = 0.0063687 -ICE LIBOR 2024-09-09 3M = 0.0063688 -ICE LIBOR 2024-09-10 3M = 0.0063689 -ICE LIBOR 2024-12-06 3M = 0.0063690 -ICE LIBOR 2024-12-07 3M = 0.0063691 -ICE LIBOR 2024-12-08 3M = 0.0063692 -ICE LIBOR 2024-12-09 3M = 0.0063693 -ICE LIBOR 2024-12-10 3M = 0.0063694 -ICE LIBOR 2025-03-06 3M = 0.0063695 -ICE LIBOR 2025-03-07 3M = 0.0063696 -ICE LIBOR 2025-03-08 3M = 0.0063697 -ICE LIBOR 2025-03-09 3M = 0.0063698 -ICE LIBOR 2025-03-10 3M = 0.0063699 -ICE LIBOR 2025-06-06 3M = 0.0063700 -ICE LIBOR 2025-06-07 3M = 0.0063701 -ICE LIBOR 2025-06-08 3M = 0.0063702 -ICE LIBOR 2025-06-09 3M = 0.0063703 -ICE LIBOR 2025-06-10 3M = 0.0063704 -ICE LIBOR 2025-09-06 3M = 0.0063705 -ICE LIBOR 2025-09-07 3M = 0.0063706 -ICE LIBOR 2025-09-08 3M = 0.0063707 -ICE LIBOR 2025-09-09 3M = 0.0063708 -ICE LIBOR 2025-09-10 3M = 0.0063709 -ICE LIBOR 2025-12-06 3M = 0.0063710 -ICE LIBOR 2025-12-07 3M = 0.0063711 -ICE LIBOR 2025-12-08 3M = 0.0063712 -ICE LIBOR 2025-12-09 3M = 0.0063713 -ICE LIBOR 2025-12-10 3M = 0.0063714 -ICE LIBOR 2026-03-06 3M = 0.0063715 -ICE LIBOR 2026-03-07 3M = 0.0063716 -ICE LIBOR 2026-03-08 3M = 0.0063717 -ICE LIBOR 2026-03-09 3M = 0.0063718 -ICE LIBOR 2026-03-10 3M = 0.0063719 -ICE LIBOR 2026-06-06 3M = 0.0063720 -ICE LIBOR 2026-06-07 3M = 0.0063721 -ICE LIBOR 2026-06-08 3M = 0.0063722 -ICE LIBOR 2026-06-09 3M = 0.0063723 -ICE LIBOR 2026-06-10 3M = 0.0063724 -ICE LIBOR 2026-09-06 3M = 0.0063725 -ICE LIBOR 2026-09-07 3M = 0.0063726 -ICE LIBOR 2026-09-08 3M = 0.0063727 -ICE LIBOR 2026-09-09 3M = 0.0063728 -ICE LIBOR 2026-09-10 3M = 0.0063729 -ICE LIBOR 2026-12-06 3M = 0.0063730 -ICE LIBOR 2026-12-07 3M = 0.0063731 -ICE LIBOR 2026-12-08 3M = 0.0063732 -ICE LIBOR 2026-12-09 3M = 0.0063733 -ICE LIBOR 2026-12-10 3M = 0.0063734 \ No newline at end of file diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt index 0c3d1fc05c..bcaf971349 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt @@ -2,7 +2,7 @@ package net.corda.irs.testing import net.corda.core.getOrThrow import net.corda.core.utilities.LogHelper -import net.corda.simulation.IRSSimulation +import net.corda.irs.simulation.IRSSimulation import org.junit.Test class IRSSimulationTest { diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt index 205f2d707d..6ca77b8058 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt @@ -16,12 +16,12 @@ import java.time.LocalDate import java.util.* import kotlin.test.assertEquals -fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { +fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { return when (irsSelect) { 1 -> { val fixedLeg = InterestRateSwap.FixedLeg( - fixedRatePayer = MEGA_CORP.toAnonymous(), + fixedRatePayer = MEGA_CORP, notional = 15900000.DOLLARS, paymentFrequency = Frequency.SemiAnnual, effectiveDate = LocalDate.of(2016, 3, 10), @@ -40,7 +40,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { ) val floatingLeg = InterestRateSwap.FloatingLeg( - floatingRatePayer = MINI_CORP.toAnonymous(), + floatingRatePayer = MINI_CORP, notional = 15900000.DOLLARS, paymentFrequency = Frequency.Quarterly, effectiveDate = LocalDate.of(2016, 3, 10), @@ -111,7 +111,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { // I did a mock up start date 10/03/2015 – 10/03/2025 so you have 5 cashflows on float side that have been preset the rest are unknown val fixedLeg = InterestRateSwap.FixedLeg( - fixedRatePayer = MEGA_CORP.toAnonymous(), + fixedRatePayer = MEGA_CORP, notional = 25000000.DOLLARS, paymentFrequency = Frequency.SemiAnnual, effectiveDate = LocalDate.of(2015, 3, 10), @@ -130,7 +130,7 @@ fun createDummyIRS(irsSelect: Int): InterestRateSwap.State { ) val floatingLeg = InterestRateSwap.FloatingLeg( - floatingRatePayer = MINI_CORP.toAnonymous(), + floatingRatePayer = MINI_CORP, notional = 25000000.DOLLARS, paymentFrequency = Frequency.Quarterly, effectiveDate = LocalDate.of(2015, 3, 10), @@ -246,8 +246,8 @@ class IRSTests { /** * Utility so I don't have to keep typing this. */ - fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { - return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance>().single() + fun singleIRS(irsSelector: Int = 1): InterestRateSwap.State { + return generateIRSTxn(irsSelector).tx.outputs.map { it.data }.filterIsInstance().single() } /** @@ -301,7 +301,7 @@ class IRSTests { var previousTXN = generateIRSTxn(1) previousTXN.toLedgerTransaction(services).verify() services.recordTransactions(previousTXN) - fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance>().single() + fun currentIRS() = previousTXN.tx.outputs.map { it.data }.filterIsInstance().single() while (true) { val nextFix: FixOf = currentIRS().nextFixingOf() ?: break @@ -381,7 +381,7 @@ class IRSTests { transaction("Fix") { input("irs post agreement") - val postAgreement = "irs post agreement".output>() + val postAgreement = "irs post agreement".output() output("irs post first fixing") { postAgreement.copy( postAgreement.fixedLeg, @@ -688,7 +688,7 @@ class IRSTests { transaction("Fix") { input("irs post agreement1") input("irs post agreement2") - val postAgreement1 = "irs post agreement1".output>() + val postAgreement1 = "irs post agreement1".output() output("irs post first fixing1") { postAgreement1.copy( postAgreement1.fixedLeg, @@ -697,7 +697,7 @@ class IRSTests { postAgreement1.common.copy(tradeID = "t1") ) } - val postAgreement2 = "irs post agreement2".output>() + val postAgreement2 = "irs post agreement2".output() output("irs post first fixing2") { postAgreement2.copy( postAgreement2.fixedLeg, diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt index 1cc89d4376..fee15b50d0 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt @@ -7,12 +7,12 @@ import net.corda.contracts.asset.`owned by` import net.corda.core.bd import net.corda.core.contracts.* import net.corda.core.crypto.MerkleTreeException -import net.corda.core.identity.Party -import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.generateKeyPair import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.TransactionBuilder +import net.corda.core.utilities.ALICE import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.LogHelper import net.corda.core.utilities.ProgressTracker @@ -24,6 +24,7 @@ import net.corda.testing.ALICE_PUBKEY import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockNetwork +import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database @@ -33,7 +34,6 @@ import org.junit.Before import org.junit.Test import java.io.Closeable import java.math.BigDecimal -import java.time.Clock import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse @@ -51,7 +51,8 @@ class NodeInterestRatesTest { val DUMMY_CASH_ISSUER_KEY = generateKeyPair() val DUMMY_CASH_ISSUER = Party(X500Name("CN=Cash issuer,O=R3,OU=corda,L=London,C=UK"), DUMMY_CASH_ISSUER_KEY.public) - val clock = Clock.systemUTC() + val dummyServices = MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + val clock get() = dummyServices.clock lateinit var oracle: NodeInterestRates.Oracle lateinit var dataSource: Closeable lateinit var database: Database @@ -71,7 +72,7 @@ class NodeInterestRatesTest { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second database.transaction { - oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY, clock).apply { knownFixes = TEST_DATA } + oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY.public, dummyServices).apply { knownFixes = TEST_DATA } } } @@ -241,5 +242,5 @@ class NodeInterestRatesTest { } } - private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE_PUBKEY `with notary` DUMMY_NOTARY) + private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `with notary` DUMMY_NOTARY) } diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt index 4221bfecc7..96d15ebb6e 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/NetworkMapVisualiser.kt @@ -15,13 +15,13 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.serialization.deserialize import net.corda.core.then import net.corda.core.utilities.ProgressTracker +import net.corda.irs.simulation.IRSSimulation +import net.corda.irs.simulation.Simulation import net.corda.netmap.VisualiserViewModel.Style import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.SessionConfirm import net.corda.node.services.statemachine.SessionEnd import net.corda.node.services.statemachine.SessionInit -import net.corda.simulation.IRSSimulation -import net.corda.simulation.Simulation import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.MockNetwork import rx.Scheduler @@ -81,8 +81,7 @@ class NetworkMapVisualiser : Application() { val simulation = viewModel.simulation // Update the white-backgrounded label indicating what flow step it's up to. - simulation.allFlowSteps.observeOn(uiThread).subscribe { step: Pair -> - val (node, change) = step + 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. @@ -219,9 +218,7 @@ class NetworkMapVisualiser : Application() { } private fun bindSidebar() { - viewModel.simulation.allFlowSteps.observeOn(uiThread).subscribe { step: Pair -> - val (node, change) = step - + viewModel.simulation.allFlowSteps.observeOn(uiThread).subscribe { (node, change) -> if (change is ProgressTracker.Change.Position) { val tracker = change.tracker.topLevelTracker if (change.newStep == ProgressTracker.DONE) { @@ -237,7 +234,7 @@ class NetworkMapVisualiser : Application() { } else if (!viewModel.trackerBoxes.containsKey(tracker)) { // New flow started up; add. val extraLabel = viewModel.simulation.extraNodeLabels[node] - val label = if (extraLabel != null) "${node.info.legalIdentity.name.toString()}: $extraLabel" else node.info.legalIdentity.name.toString() + val label = if (extraLabel != null) "${node.info.legalIdentity.name}: $extraLabel" else node.info.legalIdentity.name.toString() val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker) println("Added: $tracker, $widget") viewModel.trackerBoxes[tracker] = widget @@ -264,7 +261,7 @@ class NetworkMapVisualiser : Application() { ) ) timeline.setOnFinished { - println("Removed: ${tracker}") + println("Removed: $tracker") val vbox = viewModel.trackerBoxes.remove(tracker)?.vbox view.sidebar.children.remove(vbox) } @@ -296,7 +293,7 @@ class NetworkMapVisualiser : Application() { 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 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 diff --git a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt index abf52eb5d4..d328a0fc74 100644 --- a/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/VisualiserViewModel.kt @@ -8,7 +8,7 @@ import javafx.scene.shape.Circle import javafx.scene.shape.Line import javafx.util.Duration import net.corda.core.utilities.ProgressTracker -import net.corda.simulation.IRSSimulation +import net.corda.irs.simulation.IRSSimulation import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name import java.util.* diff --git a/samples/raft-notary-demo/build.gradle b/samples/raft-notary-demo/build.gradle index 6dd5a66d82..9e1f6d0817 100644 --- a/samples/raft-notary-demo/build.gradle +++ b/samples/raft-notary-demo/build.gradle @@ -1,3 +1,5 @@ +import net.corda.plugins.Cordform + apply plugin: 'java' apply plugin: 'kotlin' apply plugin: 'idea' @@ -6,13 +8,6 @@ apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'maven-publish' -ext { - deployTo = "./build/nodes" - notaryType = "corda.notary.validating.raft" - notaryName = "CN=Raft,O=R3,OU=corda,L=Zurich,C=CH" - advertisedNotary = "$notaryType|$notaryName" -} - configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime @@ -29,6 +24,7 @@ dependencies { compile project(':client:jfx') compile project(':client:rpc') compile project(':test-utils') + compile project(':cordform-common') // Javax is required for webapis compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" @@ -53,71 +49,15 @@ publishing { } } -task generateNotaryIdentity(type: JavaExec) { - classpath = sourceSets.main.runtimeClasspath - main = "net.corda.node.utilities.ServiceIdentityGeneratorKt" - def nodeDirs = ["$deployTo/Notary1", - "$deployTo/Notary2", - "$deployTo/Notary3"].join("|") - args = [nodeDirs, notaryType, notaryName] +task deployNodesSingle(type: Cordform, dependsOn: 'jar') { + definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } -task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar', 'generateNotaryIdentity']) { - directory deployTo - networkMap "CN=Notary 1,O=R3,OU=corda,L=London,C=UK" - node { - name "CN=Party,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" - advertisedServices = [] - p2pPort 10002 - rpcPort 10003 - cordapps = [] - rpcUsers = [['username': "demo", 'password': "demo", 'permissions': [ - 'StartFlow.net.corda.notarydemo.flows.DummyIssueAndMove', - 'StartFlow.net.corda.flows.NotaryFlow$Client' - ]]] - } - node { - name "CN=Counterparty,O=R3,OU=corda,L=London,C=UK" - nearestCity "New York" - advertisedServices = [] - p2pPort 10005 - rpcPort 10006 - cordapps = [] - } - node { - name "CN=Notary 1,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" - advertisedServices = [advertisedNotary] - p2pPort 10008 - rpcPort 10009 - cordapps = [] - notaryNodePort 11002 - } - node { - name "CN=Notary 2,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" - advertisedServices = [advertisedNotary] - p2pPort 10011 - rpcPort 10012 - cordapps = [] - notaryNodePort 11004 - notaryClusterAddresses = ["localhost:11002"] - } - node { - name "CN=Notary 3,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" - advertisedServices = [advertisedNotary] - p2pPort 10014 - rpcPort 10015 - cordapps = [] - notaryNodePort 11006 - notaryClusterAddresses = ["localhost:11002"] - } +task deployNodesRaft(type: Cordform, dependsOn: 'jar') { + definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' } task notarise(type: JavaExec) { classpath = sourceSets.main.runtimeClasspath - main = 'net.corda.notarydemo.NotaryDemoKt' + main = 'net.corda.notarydemo.NotariseKt' } - diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt new file mode 100644 index 0000000000..973162de73 --- /dev/null +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt @@ -0,0 +1,28 @@ +package net.corda.demorun + +import net.corda.node.driver.NetworkMapStartStrategy +import net.corda.node.driver.PortAllocation +import net.corda.node.driver.driver +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformNode + +fun CordformDefinition.node(configure: CordformNode.() -> Unit) = addNode { cordformNode -> cordformNode.configure() } + +fun CordformDefinition.clean() { + System.err.println("Deleting: $driverDirectory") + driverDirectory.toFile().deleteRecursively() +} + +/** + * Creates and starts all nodes required for the demo. + */ +fun CordformDefinition.runNodes() = driver( + isDebug = true, + driverDirectory = driverDirectory, + networkMapStartStrategy = NetworkMapStartStrategy.Nominated(networkMapNodeName), + portAllocation = PortAllocation.Incremental(10001) +) { + setup(this) + startNodes(nodeConfigurers.map { configurer -> CordformNode().also { configurer.accept(it) } }) + waitForAllNodesToFinish() +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt new file mode 100644 index 0000000000..e9b3b2f35d --- /dev/null +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Clean.kt @@ -0,0 +1,9 @@ +package net.corda.notarydemo + +import net.corda.demorun.clean + +fun main(args: Array) { + listOf(SingleNotaryCordform, RaftNotaryCordform).forEach { + it.clean() + } +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Main.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Main.kt deleted file mode 100644 index 83329aee23..0000000000 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Main.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.corda.notarydemo - -import net.corda.core.div -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.node.driver.driver -import net.corda.node.services.transactions.RaftValidatingNotaryService -import net.corda.nodeapi.User -import org.bouncycastle.asn1.x500.X500Name -import java.nio.file.Paths - -/** Creates and starts all nodes required for the demo. */ -fun main(args: Array) { - val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.notarydemo.flows.DummyIssueAndMove", "StartFlow.net.corda.flows.NotaryFlow\$Client"))) - driver(isDebug = true, driverDirectory = Paths.get("build") / "notary-demo-nodes") { - startNode(ALICE.name, rpcUsers = demoUser) - startNode(BOB.name) - startNotaryCluster(DUMMY_NOTARY.name, clusterSize = 3, type = RaftValidatingNotaryService.type) - waitForAllNodesToFinish() - } -} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/NotaryDemo.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt similarity index 72% rename from samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/NotaryDemo.kt rename to samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 12d84b1d4a..ccbdc5b6bf 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/NotaryDemo.kt +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -2,22 +2,16 @@ package net.corda.notarydemo import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures -import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.notUsed import net.corda.core.crypto.toStringShort -import net.corda.core.div import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.transactions.SignedTransaction -import net.corda.flows.NotaryFlow -import net.corda.nodeapi.config.SSLConfiguration +import net.corda.core.utilities.BOB import net.corda.notarydemo.flows.DummyIssueAndMove -import org.bouncycastle.asn1.x500.X500Name -import java.nio.file.Path -import java.nio.file.Paths -import kotlin.system.exitProcess +import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient fun main(args: Array) { val host = HostAndPort.fromString("localhost:10003") @@ -39,7 +33,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private val counterpartyNode by lazy { val (parties, partyUpdates) = rpc.networkMapUpdates() partyUpdates.notUsed() - parties.first { it.legalIdentity.name == X500Name("CN=Counterparty,O=R3,OU=corda,L=London,C=UK") } + parties.first { it.legalIdentity.name == BOB.name } } private companion object { @@ -84,28 +78,7 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private fun notariseTransactions(transactions: List): List { // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. @Suppress("UNSUPPORTED_FEATURE") - val signatureFutures = transactions.map { rpc.startFlow(NotaryFlow::Client, it).returnValue } + val signatureFutures = transactions.map { rpc.startFlow(::RPCStartableNotaryFlowClient, it).returnValue } return Futures.allAsList(signatureFutures).getOrThrow().map { it.map { it.by.toStringShort() }.joinToString() } } } - -private fun getCertPath(args: Array): String? { - val parser = OptionParser() - val certsPath = parser.accepts("certificates").withRequiredArg() - val options = try { - parser.parse(*args) - } catch (e: Exception) { - println(e.message) - exitProcess(1) - } - return options.valueOf(certsPath) -} - -// TODO: Take this out once we have a dedicated RPC port and allow SSL on it to be optional. -private fun sslConfigFor(nodename: String, certsPath: String?): SSLConfiguration { - return object : SSLConfiguration { - override val keyStorePassword: String = "cordacadevpass" - override val trustStorePassword: String = "trustpass" - override val certificatesDirectory: Path = if (certsPath != null) Paths.get(certsPath) else Paths.get("build") / "nodes" / nodename / "certificates" - } -} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt new file mode 100644 index 0000000000..379f7dc2e1 --- /dev/null +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/RaftNotaryCordform.kt @@ -0,0 +1,73 @@ +package net.corda.notarydemo + +import net.corda.core.crypto.appendToCommonName +import net.corda.core.div +import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.demorun.node +import net.corda.demorun.runNodes +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.RaftValidatingNotaryService +import net.corda.node.utilities.ServiceIdentityGenerator +import net.corda.nodeapi.User +import net.corda.notarydemo.flows.DummyIssueAndMove +import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformContext +import org.bouncycastle.asn1.x500.X500Name + +fun main(args: Array) = RaftNotaryCordform.runNodes() + +private val notaryNames = (1..3).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } + +object RaftNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { + private val advertisedNotary = ServiceInfo(RaftValidatingNotaryService.type, X500Name("CN=Raft,O=R3,OU=corda,L=Zurich,C=CH")) + + init { + node { + name(ALICE.name.toString()) + nearestCity("London") + p2pPort(10002) + rpcPort(10003) + rpcUsers = listOf(User("demo", "demo", setOf(startFlowPermission(), startFlowPermission())).toMap()) + } + node { + name(BOB.name.toString()) + nearestCity("New York") + p2pPort(10005) + rpcPort(10006) + } + node { + name(notaryNames[0].toString()) + nearestCity("London") + advertisedServices = listOf(advertisedNotary.toString()) + p2pPort(10009) + rpcPort(10010) + notaryNodePort(10008) + } + node { + name(notaryNames[1].toString()) + nearestCity("London") + advertisedServices = listOf(advertisedNotary.toString()) + p2pPort(10013) + rpcPort(10014) + notaryNodePort(10012) + notaryClusterAddresses = listOf("localhost:10008") + } + node { + name(notaryNames[2].toString()) + nearestCity("London") + advertisedServices = listOf(advertisedNotary.toString()) + p2pPort(10017) + rpcPort(10018) + notaryNodePort(10016) + notaryClusterAddresses = listOf("localhost:10008") + } + } + + override fun setup(context: CordformContext) { + ServiceIdentityGenerator.generateToDisk(notaryNames.map { context.baseDirectory(it) }, advertisedNotary.type.id, advertisedNotary.name!!) + } +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt new file mode 100644 index 0000000000..b02d4f04c9 --- /dev/null +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/SingleNotaryCordform.kt @@ -0,0 +1,46 @@ +package net.corda.notarydemo + +import net.corda.core.div +import net.corda.core.node.services.ServiceInfo +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.demorun.node +import net.corda.demorun.runNodes +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.ValidatingNotaryService +import net.corda.nodeapi.User +import net.corda.notarydemo.flows.DummyIssueAndMove +import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient +import net.corda.cordform.CordformDefinition +import net.corda.cordform.CordformContext + +fun main(args: Array) = SingleNotaryCordform.runNodes() + +object SingleNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", DUMMY_NOTARY.name) { + init { + node { + name(ALICE.name.toString()) + nearestCity("London") + p2pPort(10002) + rpcPort(10003) + rpcUsers = listOf(User("demo", "demo", setOf(startFlowPermission(), startFlowPermission())).toMap()) + } + node { + name(BOB.name.toString()) + nearestCity("New York") + p2pPort(10005) + rpcPort(10006) + } + node { + name(DUMMY_NOTARY.name.toString()) + nearestCity("London") + advertisedServices = listOf(ServiceInfo(ValidatingNotaryService.type).toString()) + p2pPort(10009) + rpcPort(10010) + notaryNodePort(10008) + } + } + + override fun setup(context: CordformContext) {} +} diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt index 103d2fe57c..cb7bb6196c 100644 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/DummyIssueAndMove.kt @@ -4,26 +4,25 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.DummyContract import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.transactions.SignedTransaction import java.util.* +@StartableByRPC class DummyIssueAndMove(private val notary: Party, private val counterpartyNode: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val random = Random() - val myKeyPair = serviceHub.legalIdentityKey // Self issue an asset - val issueTx = DummyContract.generateInitial(random.nextInt(), notary, serviceHub.myInfo.legalIdentity.ref(0)).apply { - signWith(myKeyPair) - } - serviceHub.recordTransactions(issueTx.toSignedTransaction()) + val issueTxBuilder = DummyContract.generateInitial(random.nextInt(), notary, serviceHub.myInfo.legalIdentity.ref(0)) + val issueTx = serviceHub.signInitialTransaction(issueTxBuilder) + serviceHub.recordTransactions(issueTx) // Move ownership of the asset to the counterparty val counterPartyKey = counterpartyNode.owningKey - val asset = issueTx.toWireTransaction().outRef(0) - val moveTx = DummyContract.move(asset, counterPartyKey).apply { - signWith(myKeyPair) - } + val asset = issueTx.tx.outRef(0) + val moveTxBuilder = DummyContract.move(asset, counterpartyNode) + val moveTx = serviceHub.signInitialTransaction(moveTxBuilder) // We don't check signatures because we know that the notary's signature is missing - return moveTx.toSignedTransaction(checkSufficientSignatures = false) + return moveTx } } diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt new file mode 100644 index 0000000000..a3d16c4984 --- /dev/null +++ b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/flows/RPCStartableNotaryFlowClient.kt @@ -0,0 +1,8 @@ +package net.corda.notarydemo.flows + +import net.corda.core.flows.StartableByRPC +import net.corda.core.transactions.SignedTransaction +import net.corda.flows.NotaryFlow + +@StartableByRPC +class RPCStartableNotaryFlowClient(stx: SignedTransaction) : NotaryFlow.Client(stx) diff --git a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt b/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt deleted file mode 100644 index 816faa655a..0000000000 --- a/samples/raft-notary-demo/src/main/kotlin/net/corda/notarydemo/plugin/NotaryDemoPlugin.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.corda.notarydemo.plugin - -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.transactions.SignedTransaction -import net.corda.flows.NotaryFlow -import net.corda.notarydemo.flows.DummyIssueAndMove - -class NotaryDemoPlugin : CordaPluginRegistry() { - // A list of protocols that are required for this cordapp - override val requiredFlows = mapOf( - NotaryFlow.Client::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name), - DummyIssueAndMove::class.java.name to setOf(Party::class.java.name) - ) -} diff --git a/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 2ea555bbb9..0000000000 --- a/samples/raft-notary-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.node.CordaPluginRegistry -net.corda.notarydemo.plugin.NotaryDemoPlugin \ No newline at end of file diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index cd6cd786ff..59bd02d389 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -108,9 +108,11 @@ task npmInstall(type: Exec) { outputs.upToDateWhen { file('src/main/web/node_modules').exists() } } -task cleanWeb() << { - delete 'src/main/resources/simmvaluationweb' - delete 'src/main/web/dist' +task cleanWeb() { + doLast { + delete 'src/main/resources/simmvaluationweb' + delete 'src/main/web/dist' + } } task buildWeb(type: Exec, dependsOn: [cleanWeb, npmInstall]) { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt index 05f4b8075d..35898c508d 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/IRSState.kt @@ -4,9 +4,9 @@ import net.corda.core.contracts.Command import net.corda.core.contracts.DealState import net.corda.core.contracts.TransactionType import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party import net.corda.core.crypto.keys +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -16,12 +16,12 @@ import java.security.PublicKey * TODO: Merge with the existing demo IRS code. */ data class IRSState(val swap: SwapData, - val buyer: AnonymousParty, - val seller: AnonymousParty, + val buyer: AbstractParty, + val seller: AbstractParty, override val contract: OGTrade, override val linearId: UniqueIdentifier = UniqueIdentifier(swap.id.first + swap.id.second)) : DealState { override val ref: String = linearId.externalId!! // Same as the constructor for UniqueIdentified - override val parties: List get() = listOf(buyer, seller) + override val parties: List get() = listOf(buyer, seller) override fun isRelevant(ourKeys: Set): Boolean { return parties.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() @@ -32,6 +32,6 @@ data class IRSState(val swap: SwapData, return TransactionType.General.Builder(notary).withItems(state, Command(OGTrade.Commands.Agree(), parties.map { it.owningKey })) } - override val participants: List - get() = parties.map { it.owningKey } + override val participants: List + get() = parties } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt index 4d036a92a0..97cc7dc43b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/OGTrade.kt @@ -45,7 +45,7 @@ data class OGTrade(override val legalContractReference: SecureHash = SecureHash. require(inputs.size == 0) { "Inputs must be empty" } require(outputs.size == 1) { "" } require(outputs[0].buyer != outputs[0].seller) - require(outputs[0].parties.map { it.owningKey }.containsAll(outputs[0].participants)) + require(outputs[0].parties.containsAll(outputs[0].participants)) require(outputs[0].parties.containsAll(listOf(outputs[0].buyer, outputs[0].seller))) require(outputs[0].swap.startDate.isBefore(outputs[0].swap.endDate)) require(outputs[0].swap.notional > BigDecimal(0)) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt index 6677ebd882..083fe655bd 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioState.kt @@ -1,10 +1,10 @@ package net.corda.vega.contracts import net.corda.core.contracts.* -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogicRefFactory +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder import net.corda.vega.flows.SimmRevaluation @@ -19,7 +19,7 @@ import java.time.temporal.ChronoUnit */ data class PortfolioState(val portfolio: List, override val contract: PortfolioSwap, - private val _parties: Pair, + private val _parties: Pair, val valuationDate: LocalDate, val valuation: PortfolioValuation? = null, override val linearId: UniqueIdentifier = UniqueIdentifier()) @@ -27,12 +27,12 @@ data class PortfolioState(val portfolio: List, @CordaSerializable data class Update(val portfolio: List? = null, val valuation: PortfolioValuation? = null) - override val parties: List get() = _parties.toList() + override val parties: List get() = _parties.toList() override val ref: String = linearId.toString() - val valuer: AnonymousParty get() = parties[0] + val valuer: AbstractParty get() = parties[0] - override val participants: List - get() = parties.map { it.owningKey } + override val participants: List + get() = parties override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity { val flow = flowLogicRefFactory.create(SimmRevaluation.Initiator::class.java, thisStateRef, LocalDate.now()) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt index 809b426e84..4dad44ad41 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/contracts/PortfolioSwap.kt @@ -71,7 +71,7 @@ data class PortfolioSwap(override val legalContractReference: SecureHash = Secur "there are no inputs" using (inputs.size == 0) "there is one output" using (outputs.size == 1) "valuer must be a party" using (outputs[0].parties.contains(outputs[0].valuer)) - "all participants must be parties" using (outputs[0].parties.map { it.owningKey }.containsAll(outputs[0].participants)) + "all participants must be parties" using (outputs[0].parties.containsAll(outputs[0].participants)) } return setOf(command.value) diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt index 3959dcb127..11a8363f92 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/IRSTradeFlow.kt @@ -1,9 +1,10 @@ package net.corda.vega.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction @@ -24,6 +25,7 @@ object IRSTradeFlow { data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) @InitiatingFlow + @StartableByRPC class Requester(val swap: SwapData, val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -36,7 +38,7 @@ object IRSTradeFlow { } else { Pair(otherParty, myIdentity) } - val offer = IRSState(swap, buyer.toAnonymous(), seller.toAnonymous(), OGTrade()) + val offer = IRSState(swap, buyer, seller, OGTrade()) logger.info("Handshake finished, sending IRS trade offer message") val otherPartyAgreeFlag = sendAndReceive(otherParty, OfferMessage(notary, offer)).unwrap { it } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt index c2c764013f..94061ba0d5 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmFlow.kt @@ -10,10 +10,11 @@ import com.opengamma.strata.pricer.rate.ImmutableRatesProvider import com.opengamma.strata.pricer.swap.DiscountingSwapProductPricer import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party import net.corda.core.node.PluginServiceHub import net.corda.core.node.services.dealsWith import net.corda.core.serialization.CordaSerializable @@ -51,6 +52,7 @@ object SimmFlow { * margin using SIMM. If there is an existing state it will update and revalue the portfolio agreement. */ @InitiatingFlow + @StartableByRPC class Requester(val otherParty: Party, val valuationDate: LocalDate, val existing: StateAndRef?) @@ -83,7 +85,7 @@ object SimmFlow { @Suspendable private fun agreePortfolio(portfolio: Portfolio) { logger.info("Agreeing portfolio") - val parties = Pair(myIdentity.toAnonymous(), otherParty.toAnonymous()) + val parties = Pair(myIdentity, otherParty) val portfolioState = PortfolioState(portfolio.refs, PortfolioSwap(), parties, valuationDate) send(otherParty, OfferMessage(notary, portfolioState, existing?.ref, valuationDate)) @@ -217,13 +219,6 @@ object SimmFlow { return receive(replyToParty).unwrap { it } } - @Suspendable - private fun agreeValuation(portfolio: Portfolio, asOf: LocalDate, valuer: AnonymousParty): PortfolioValuation { - val valuerParty = serviceHub.identityService.partyFromAnonymous(valuer) - require(valuerParty != null) - return agreeValuation(portfolio, asOf, valuerParty!!) - } - /** * So this is the crux of the Simm Agreement flow * It needs to do several things - which are mainly defined by the analytics engine we are using - which in this @@ -324,7 +319,7 @@ object SimmFlow { @Suspendable private fun updateValuation(stateRef: StateAndRef) { val portfolio = stateRef.state.data.portfolio.toStateAndRef(serviceHub).toPortfolio() - val valuer = stateRef.state.data.valuer + val valuer = serviceHub.identityService.partyFromAnonymous(stateRef.state.data.valuer) ?: throw IllegalStateException("Unknown valuer party ${stateRef.state.data.valuer}") val valuation = agreeValuation(portfolio, offer.valuationDate, valuer) subFlow(object : StateRevisionFlow.Receiver(replyToParty) { override fun verifyProposal(proposal: Proposal) { diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt index 4580e568be..e687fd710f 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/SimmRevaluation.kt @@ -3,6 +3,8 @@ package net.corda.vega.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.StateRef import net.corda.core.flows.FlowLogic +import net.corda.core.flows.SchedulableFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.node.services.linearHeadsOfType import net.corda.vega.contracts.PortfolioState import java.time.LocalDate @@ -12,12 +14,14 @@ import java.time.LocalDate * requirements */ object SimmRevaluation { + @StartableByRPC + @SchedulableFlow class Initiator(val curStateRef: StateRef, val valuationDate: LocalDate) : FlowLogic() { @Suspendable override fun call(): Unit { val stateAndRef = serviceHub.vaultService.linearHeadsOfType().values.first { it.ref == curStateRef } val curState = stateAndRef.state.data - val myIdentity = serviceHub.myInfo.legalIdentity.toAnonymous() + val myIdentity = serviceHub.myInfo.legalIdentity if (myIdentity == curState.parties[0]) { val otherParty = serviceHub.identityService.partyFromAnonymous(curState.parties[1]) require(otherParty != null) { "Other party must be known by this node" } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt index f4e3108fcf..d31711549b 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/flows/StateRevisionFlow.kt @@ -1,6 +1,7 @@ package net.corda.vega.flows import net.corda.core.contracts.StateAndRef +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction @@ -16,13 +17,12 @@ import java.security.PublicKey object StateRevisionFlow { class Requester(curStateRef: StateAndRef>, updatedData: T) : AbstractStateReplacementFlow.Instigator, RevisionedState, T>(curStateRef, updatedData) { - override fun assembleTx(): Pair> { + override fun assembleTx(): Pair> { val state = originalState.state.data val tx = state.generateRevision(originalState.state.notary, originalState, modification) tx.setTime(serviceHub.clock.instant(), 30.seconds) - tx.signWith(serviceHub.legalIdentityKey) - val stx = tx.toSignedTransaction(false) + val stx = serviceHub.signInitialTransaction(tx) return Pair(stx, state.participants) } } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt index d6d9aef072..06d14023d7 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/services/SimmService.kt @@ -10,18 +10,14 @@ import com.opengamma.strata.market.curve.CurveName import com.opengamma.strata.market.param.CurrencyParameterSensitivities import com.opengamma.strata.market.param.CurrencyParameterSensitivity import com.opengamma.strata.market.param.TenorDateParameterMetadata -import net.corda.core.contracts.StateRef import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.core.serialization.SerializationCustomization import net.corda.vega.analytics.CordaMarketData import net.corda.vega.analytics.InitialMarginTriple import net.corda.vega.api.PortfolioApi -import net.corda.vega.contracts.SwapData import net.corda.vega.flows.IRSTradeFlow import net.corda.vega.flows.SimmFlow -import net.corda.vega.flows.SimmRevaluation -import java.time.LocalDate import java.util.function.Function /** @@ -32,10 +28,6 @@ import java.util.function.Function object SimmService { class Plugin : CordaPluginRegistry() { override val webApis = listOf(Function(::PortfolioApi)) - override val requiredFlows: Map> = mapOf( - SimmFlow.Requester::class.java.name to setOf(Party::class.java.name, LocalDate::class.java.name), - SimmRevaluation.Initiator::class.java.name to setOf(StateRef::class.java.name, LocalDate::class.java.name), - IRSTradeFlow.Requester::class.java.name to setOf(SwapData::class.java.name, Party::class.java.name)) override val staticServeDirs: Map = mapOf("simmvaluationdemo" to javaClass.classLoader.getResource("simmvaluationweb").toExternalForm()) override val servicePlugins = listOf(Function(SimmFlow::Service), Function(IRSTradeFlow::Service)) override fun customizeSerialization(custom: SerializationCustomization): Boolean { diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt index a5af0ab52c..1660ae7bfa 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt @@ -11,7 +11,6 @@ import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC -import java.nio.file.Paths /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) @@ -22,7 +21,7 @@ fun main(args: Array) { startFlowPermission(), startFlowPermission()) val demoUser = listOf(User("demo", "demo", permissions)) - driver(driverDirectory = Paths.get("build") / "trader-demo-nodes", isDebug = true) { + driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true) { val user = User("user1", "test", permissions = setOf(startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt index d2bee38d56..8a6239684b 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/SellerFlow.kt @@ -4,12 +4,14 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.* -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.days import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.identity.Party +import net.corda.core.identity.AbstractParty import net.corda.core.node.NodeInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction @@ -22,6 +24,7 @@ import java.time.Instant import java.util.* @InitiatingFlow +@StartableByRPC class SellerFlow(val otherParty: Party, val amount: Amount, override val progressTracker: ProgressTracker) : FlowLogic() { @@ -48,7 +51,7 @@ class SellerFlow(val otherParty: Party, val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] val cpOwnerKey = serviceHub.legalIdentityKey - val commercialPaper = selfIssueSomeCommercialPaper(cpOwnerKey.public, notary) + val commercialPaper = selfIssueSomeCommercialPaper(serviceHub.myInfo.legalIdentity, notary) progressTracker.currentStep = TRADING @@ -65,7 +68,7 @@ class SellerFlow(val otherParty: Party, } @Suspendable - fun selfIssueSomeCommercialPaper(ownedBy: PublicKey, notaryNode: NodeInfo): StateAndRef { + fun selfIssueSomeCommercialPaper(ownedBy: AbstractParty, notaryNode: NodeInfo): StateAndRef { // Make a fake company that's issued its own paper. val keyPair = generateKeyPair() val party = Party(BOC.name, keyPair.public) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt index 8fa30e3e10..cd45ed4b95 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt @@ -1,16 +1,10 @@ package net.corda.traderdemo.plugin -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry import net.corda.traderdemo.flow.BuyerFlow -import net.corda.traderdemo.flow.SellerFlow import java.util.function.Function class TraderDemoPlugin : CordaPluginRegistry() { - // A list of Flows that are required for this cordapp - override val requiredFlows: Map> = mapOf( - SellerFlow::class.java.name to setOf(Party::class.java.name, Amount::class.java.name) - ) override val servicePlugins = listOf(Function(BuyerFlow::Service)) } diff --git a/settings.gradle b/settings.gradle index f20ee05323..90b1e5797d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,6 +17,7 @@ include 'webserver' include 'webserver:webcapsule' include 'experimental' include 'experimental:sandbox' +include 'experimental:quasar-hook' include 'verifier' include 'test-utils' include 'tools:explorer' @@ -31,5 +32,6 @@ include 'samples:network-visualiser' include 'samples:simm-valuation-demo' include 'samples:raft-notary-demo' include 'samples:bank-of-corda-demo' +include 'cordform-common' include 'doorman' include 'verify-enclave' diff --git a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt index a86f941765..13afa5e73a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -11,6 +11,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.VersionInfo +import net.corda.core.node.services.IdentityService import net.corda.core.serialization.OpaqueBytes import net.corda.core.toFuture import net.corda.core.transactions.TransactionBuilder @@ -18,17 +19,16 @@ import net.corda.core.utilities.* import net.corda.node.internal.AbstractNode import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.* +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration -import net.corda.testing.node.MockIdentityService import net.corda.testing.node.MockServices import net.corda.testing.node.makeTestDataSourceProperties import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500NameBuilder import org.bouncycastle.asn1.x500.style.BCStyle -import java.net.ServerSocket import java.net.URL import java.nio.file.Files import java.nio.file.Path @@ -85,7 +85,7 @@ val BIG_CORP_PARTY_REF = BIG_CORP.ref(OpaqueBytes.of(1)).reference val ALL_TEST_KEYS: List get() = listOf(MEGA_CORP_KEY, MINI_CORP_KEY, ALICE_KEY, BOB_KEY, DUMMY_NOTARY_KEY) -val MOCK_IDENTITY_SERVICE: MockIdentityService get() = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) +val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") diff --git a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt index 6822c1fa76..5bc7af456e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -9,6 +9,7 @@ import net.corda.client.mock.string import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.div +import net.corda.core.map import net.corda.core.messaging.RPCOps import net.corda.core.random63BitValue import net.corda.core.utilities.ProcessUtilities @@ -64,7 +65,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, ops : I - ): ListenableFuture + ): ListenableFuture /** * Starts an In-VM RPC client. @@ -108,6 +109,7 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, configuration: RPCServerConfiguration = RPCServerConfiguration.default, + customPort: HostAndPort? = null, ops : I ) : ListenableFuture @@ -155,6 +157,28 @@ interface RPCDriverExposedDSLInterface : DriverDSLExposedInterface { username: String = rpcTestUser.username, password: String = rpcTestUser.password ): ClientSession + + fun startRpcBroker( + serverName: String = "driver-rpc-server-${random63BitValue()}", + rpcUser: User = rpcTestUser, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE, + customPort: HostAndPort? = null + ): ListenableFuture + + fun startInVmRpcBroker( + rpcUser: User = rpcTestUser, + maxFileSize: Int = ArtemisMessagingServer.MAX_FILE_SIZE, + maxBufferedBytesPerClient: Long = 10L * ArtemisMessagingServer.MAX_FILE_SIZE + ): ListenableFuture + + fun startRpcServerWithBrokerRunning( + rpcUser: User = rpcTestUser, + nodeLegalName: X500Name = fakeNodeLegalName, + configuration: RPCServerConfiguration = RPCServerConfiguration.default, + ops: I, + brokerHandle: RpcBrokerHandle + ): RpcServerHandle } inline fun RPCDriverExposedDSLInterface.startInVmRpcClient( username: String = rpcTestUser.username, @@ -175,11 +199,17 @@ inline fun RPCDriverExposedDSLInterface.startRpcClient( interface RPCDriverInternalDSLInterface : DriverDSLInternalInterface, RPCDriverExposedDSLInterface -data class RpcServerHandle( - val hostAndPort: HostAndPort, +data class RpcBrokerHandle( + val hostAndPort: HostAndPort?, /** null if this is an InVM broker */ + val clientTransportConfiguration: TransportConfiguration, val serverControl: ActiveMQServerControl ) +data class RpcServerHandle( + val broker: RpcBrokerHandle, + val rpcServer: RPCServer +) + val rpcTestUser = User("user1", "test", permissions = emptySet()) val fakeNodeLegalName = X500Name("CN=not:a:valid:name") @@ -193,7 +223,7 @@ fun rpcDriver( debugPortAllocation: PortAllocation = globalDebugPortAllocation, systemProperties: Map = emptyMap(), useTestClock: Boolean = false, - automaticallyStartNetworkMap: Boolean = false, + networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), dsl: RPCDriverExposedDSLInterface.() -> A ) = genericDriver( driverDsl = RPCDriverDSL( @@ -203,7 +233,7 @@ fun rpcDriver( systemProperties = systemProperties, driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, - automaticallyStartNetworkMap = automaticallyStartNetworkMap, + networkMapStartStrategy = networkMapStartStrategy, isDebug = isDebug ) ), @@ -292,21 +322,9 @@ data class RPCDriverDSL( maxBufferedBytesPerClient: Long, configuration: RPCServerConfiguration, ops: I - ): ListenableFuture { - return driverDSL.executorService.submit { - val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient) - val server = EmbeddedActiveMQ() - server.setConfiguration(artemisConfig) - server.setSecurityManager(SingleUserSecurityManager(rpcUser)) - server.start() - driverDSL.shutdownManager.registerShutdown { - server.activeMQServer.stop() - server.stop() - } - startRpcServerWithBrokerRunning( - rpcUser, nodeLegalName, configuration, ops, inVmClientTransportConfiguration, - server.activeMQServer.activeMQServerControl - ) + ): ListenableFuture { + return startInVmRpcBroker(rpcUser, maxFileSize, maxBufferedBytesPerClient).map { broker -> + startRpcServerWithBrokerRunning(rpcUser, nodeLegalName, configuration, ops, broker) } } @@ -340,24 +358,11 @@ data class RPCDriverDSL( maxFileSize: Int, maxBufferedBytesPerClient: Long, configuration: RPCServerConfiguration, + customPort: HostAndPort?, ops: I ): ListenableFuture { - val hostAndPort = driverDSL.portAllocation.nextHostAndPort() - addressMustNotBeBound(driverDSL.executorService, hostAndPort) - return driverDSL.executorService.submit { - val artemisConfig = createRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient, driverDSL.driverDirectory / serverName, hostAndPort) - val server = ActiveMQServerImpl(artemisConfig, SingleUserSecurityManager(rpcUser)) - server.start() - driverDSL.shutdownManager.registerShutdown { - server.stop() - addressMustNotBeBound(driverDSL.executorService, hostAndPort).get() - } - val transportConfiguration = createNettyClientTransportConfiguration(hostAndPort) - startRpcServerWithBrokerRunning( - rpcUser, nodeLegalName, configuration, ops, transportConfiguration, - server.activeMQServerControl - ) - RpcServerHandle(hostAndPort, server.activeMQServerControl) + return startRpcBroker(serverName, rpcUser, maxFileSize, maxBufferedBytesPerClient, customPort).map { broker -> + startRpcServerWithBrokerRunning(rpcUser, nodeLegalName, configuration, ops, broker) } } @@ -399,16 +404,58 @@ data class RPCDriverDSL( return session } + override fun startRpcBroker( + serverName: String, + rpcUser: User, + maxFileSize: Int, + maxBufferedBytesPerClient: Long, + customPort: HostAndPort? + ): ListenableFuture { + val hostAndPort = customPort ?: driverDSL.portAllocation.nextHostAndPort() + addressMustNotBeBound(driverDSL.executorService, hostAndPort) + return driverDSL.executorService.submit { + val artemisConfig = createRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient, driverDSL.driverDirectory / serverName, hostAndPort) + val server = ActiveMQServerImpl(artemisConfig, SingleUserSecurityManager(rpcUser)) + server.start() + driverDSL.shutdownManager.registerShutdown { + server.stop() + addressMustNotBeBound(driverDSL.executorService, hostAndPort).get() + } + RpcBrokerHandle( + hostAndPort = hostAndPort, + clientTransportConfiguration = createNettyClientTransportConfiguration(hostAndPort), + serverControl = server.activeMQServerControl + ) + } + } - private fun startRpcServerWithBrokerRunning( + override fun startInVmRpcBroker(rpcUser: User, maxFileSize: Int, maxBufferedBytesPerClient: Long): ListenableFuture { + return driverDSL.executorService.submit { + val artemisConfig = createInVmRpcServerArtemisConfig(maxFileSize, maxBufferedBytesPerClient) + val server = EmbeddedActiveMQ() + server.setConfiguration(artemisConfig) + server.setSecurityManager(SingleUserSecurityManager(rpcUser)) + server.start() + driverDSL.shutdownManager.registerShutdown { + server.activeMQServer.stop() + server.stop() + } + RpcBrokerHandle( + hostAndPort = null, + clientTransportConfiguration = inVmClientTransportConfiguration, + serverControl = server.activeMQServer.activeMQServerControl + ) + } + } + + override fun startRpcServerWithBrokerRunning( rpcUser: User, nodeLegalName: X500Name, configuration: RPCServerConfiguration, ops: I, - transportConfiguration: TransportConfiguration, - serverControl: ActiveMQServerControl - ) { - val locator = ActiveMQClient.createServerLocatorWithoutHA(transportConfiguration).apply { + brokerHandle: RpcBrokerHandle + ): RpcServerHandle { + val locator = ActiveMQClient.createServerLocatorWithoutHA(brokerHandle.clientTransportConfiguration).apply { minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE } val userService = object : RPCUserService { @@ -428,7 +475,8 @@ data class RPCDriverDSL( rpcServer.close() locator.close() } - rpcServer.start(serverControl) + rpcServer.start(brokerHandle.serverControl) + return RpcServerHandle(brokerHandle, rpcServer) } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt index 0dd8cda938..27d69dcc2e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -79,7 +79,7 @@ class TransactionDSL(val interpreter: T) : Tr val transaction = ledgerInterpreter._unverifiedTransaction(null, TransactionBuilder(notary = DUMMY_NOTARY)) { output { state } // Add a dummy randomised output so that the transaction id differs when issuing the same state multiple times - val nonceState = DummyContract.SingleOwnerState(Random().nextInt(), DUMMY_NOTARY.owningKey) + val nonceState = DummyContract.SingleOwnerState(Random().nextInt(), DUMMY_NOTARY) output { nonceState } } input(transaction.outRef(0).ref) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt index 3a549fad6c..4905acec1d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNode.kt @@ -20,6 +20,7 @@ import net.corda.core.utilities.loggerFor import net.corda.node.internal.AbstractNode import net.corda.node.internal.ServiceFlowInfo import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.MessagingService import net.corda.node.services.network.InMemoryNetworkMapService @@ -166,7 +167,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, .getOrThrow() } - override fun makeIdentityService() = MockIdentityService(mockNet.identities) + override fun makeIdentityService() = InMemoryIdentityService(mockNet.identities) override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt index c9e8eb5e55..7eb9e8c5dc 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockServices.kt @@ -4,6 +4,7 @@ import net.corda.core.contracts.Attachment import net.corda.core.contracts.PartyAndReference import net.corda.core.crypto.* import net.corda.core.flows.StateMachineRunId +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.messaging.SingleMessageRecipient @@ -13,6 +14,7 @@ import net.corda.core.node.services.* import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService @@ -32,8 +34,11 @@ import java.nio.file.Paths import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.time.Clock import java.util.* +import java.util.concurrent.ConcurrentHashMap import java.util.jar.JarInputStream import javax.annotation.concurrent.ThreadSafe @@ -44,7 +49,11 @@ import javax.annotation.concurrent.ThreadSafe * A singleton utility that only provides a mock identity, key and storage service. However, this is sufficient for * building chains of transactions and verifying them. It isn't sufficient for testing flows however. */ -open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub { +open class MockServices(vararg val keys: KeyPair) : ServiceHub { + constructor() : this(generateKeyPair()) + + val key: KeyPair get() = keys.first() + override fun recordTransactions(txs: Iterable) { txs.forEach { storageService.stateMachineRecordedTransactionMapping.addMapping(StateMachineRunId.createRandom(), it.id) @@ -55,8 +64,8 @@ open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub { } override val storageService: TxWritableStorageService = MockStorageService() - override val identityService: MockIdentityService = MockIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) - override val keyManagementService: MockKeyManagementService = MockKeyManagementService(key) + override val identityService: IdentityService = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) + override val keyManagementService: KeyManagementService = MockKeyManagementService(*keys) override val vaultService: VaultService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() @@ -72,35 +81,28 @@ open class MockServices(val key: KeyPair = generateKeyPair()) : ServiceHub { } } -@ThreadSafe -class MockIdentityService(val identities: List) : IdentityService, SingletonSerializeAsToken() { - private val keyToParties: Map - get() = synchronized(identities) { identities.associateBy { it.owningKey } } - private val nameToParties: Map - get() = synchronized(identities) { identities.associateBy { it.name } } - - override fun registerIdentity(party: Party) { - throw UnsupportedOperationException() - } - - override fun getAllIdentities(): Iterable = ArrayList(keyToParties.values) - override fun partyFromAnonymous(party: AnonymousParty): Party? = keyToParties[party.owningKey] - override fun partyFromAnonymous(partyRef: PartyAndReference): Party? = partyFromAnonymous(partyRef.party) - override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] - override fun partyFromName(name: String): Party? = nameToParties[X500Name(name)] - override fun partyFromX500Name(principal: X500Name): Party? = nameToParties[principal] -} - - class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { - override val keys: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) + private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) + + override val keys: Set get() = keyStore.keys val nextKeys = LinkedList() - override fun freshKey(): KeyPair { + override fun freshKey(): PublicKey { val k = nextKeys.poll() ?: generateKeyPair() - keys[k.public] = k.private - return k + keyStore[k.public] = k.private + return k.public + } + + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { + val pk = publicKey.keys.first { keyStore.containsKey(it) } + return KeyPair(pk, keyStore[pk]!!) + } + + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { + val keyPair = getSigningKeyPair(publicKey) + val signature = keyPair.sign(bytes) + return signature } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt index cf93e05ebe..0d58b85366 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/NodeBasedTest.kt @@ -12,6 +12,8 @@ import net.corda.node.driver.addressMustNotBeBound 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.config.configOf +import net.corda.node.services.config.plus import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User @@ -141,18 +143,12 @@ abstract class NodeBasedTest { val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory, allowMissingConfig = true, - configOverrides = mapOf( + configOverrides = configOf( "myLegalName" to legalName.toString(), "p2pAddress" to localPort[0].toString(), "rpcAddress" to localPort[1].toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, - "rpcUsers" to rpcUsers.map { - mapOf( - "username" to it.username, - "password" to it.password, - "permissions" to it.permissions - ) - } + "rpcUsers" to rpcUsers.map { it.toMap() } ) + configOverrides ) diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt index 80b51f99a6..29c4d8042d 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/SimpleNode.kt @@ -6,18 +6,19 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.commonName import net.corda.core.crypto.generateKeyPair import net.corda.core.messaging.RPCOps -import net.corda.testing.MOCK_VERSION_INFO +import net.corda.core.node.services.KeyManagementService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.api.MonitoringService import net.corda.node.services.config.NodeConfiguration +import net.corda.node.services.keys.E2ETestKeyManagementService 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.utilities.AffinityExecutor.ServiceAffinityExecutor import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction +import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.freeLocalHostAndPort -import org.bouncycastle.asn1.x500.X500Name import org.jetbrains.exposed.sql.Database import java.io.Closeable import java.security.KeyPair @@ -34,6 +35,7 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL val userService = RPCUserServiceImpl(config.rpcUsers) val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() + val keyService: KeyManagementService = E2ETestKeyManagementService(setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) val broker = ArtemisMessagingServer(config, address, rpcAddress, InMemoryNetworkMapCache(), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create() diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 969d762e88..6005379e4b 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -127,7 +127,7 @@ distributions { * Bundles the application using JavaPackager, * using the ZIP distribution as source. */ -task javapackage(dependsOn: 'distZip') { +task javapackage(dependsOn: distZip) { doLast { delete([pkg_source, pkg_outDir]) diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt index bd08931e76..9aef0146f8 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NetworkMapConfig.kt @@ -1,6 +1,7 @@ package net.corda.demobench.model import net.corda.core.crypto.commonName +import net.corda.core.utilities.WHITESPACE import org.bouncycastle.asn1.x500.X500Name open class NetworkMapConfig(val legalName: X500Name, val p2pPort: Int) { @@ -9,7 +10,5 @@ open class NetworkMapConfig(val legalName: X500Name, val p2pPort: Int) { } -private val WHITESPACE = "\\s++".toRegex() - -fun String.stripWhitespace() = this.replace(WHITESPACE, "") -fun String.toKey() = this.stripWhitespace().toLowerCase() +fun String.stripWhitespace() = replace(WHITESPACE, "") +fun String.toKey() = stripWhitespace().toLowerCase() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt index 1c98d2e0bc..7a73d40e28 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeConfig.kt @@ -4,7 +4,6 @@ import com.typesafe.config.* import net.corda.core.crypto.location import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x500.style.BCStyle import java.io.File import java.nio.file.Files import java.nio.file.Path diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt index fe46a5acda..31460611d9 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeController.kt @@ -1,12 +1,11 @@ package net.corda.demobench.model +import net.corda.core.crypto.X509Utilities.getX509Name import net.corda.demobench.plugin.PluginController import net.corda.demobench.pty.R3Pty -import org.bouncycastle.asn1.x500.X500Name import tornadofx.* import java.io.IOException import java.lang.management.ManagementFactory -import java.net.ServerSocket import java.nio.file.Files import java.nio.file.Path import java.text.SimpleDateFormat @@ -50,9 +49,15 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { * Validate a Node configuration provided by [net.corda.demobench.views.NodeTabView]. */ fun validate(nodeData: NodeData): NodeConfig? { + val location = nodeData.nearestCity.value val config = NodeConfig( baseDir, - X500Name(nodeData.legalName.value.trim()), + getX509Name( + myLegalName = nodeData.legalName.value.trim(), + email = "corda@city.${location.countryCode.toLowerCase()}.example", + nearestCity = location.description, + country = location.countryCode + ), nodeData.p2pPort.value, nodeData.rpcPort.value, nodeData.webPort.value, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt index 7c821b65b0..aadbeb7e06 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/User.kt @@ -5,12 +5,6 @@ package net.corda.demobench.model import net.corda.nodeapi.User import java.util.* -fun User.toMap(): Map = mapOf( - "username" to username, - "password" to password, - "permissions" to permissions -) - @Suppress("UNCHECKED_CAST") fun toUser(map: Map) = User( map.getOrElse("username", { "none" }) as String, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt index feaa423299..68152fbfc1 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/pty/R3Pty.kt @@ -11,7 +11,7 @@ import java.awt.Dimension import java.io.IOException import java.nio.charset.StandardCharsets.UTF_8 import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeUnit.SECONDS class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension, val onExit: (Int) -> Unit) : AutoCloseable { private companion object { @@ -37,7 +37,7 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension return PtyProcessTtyConnector(name.commonName, process, UTF_8) } catch (e: Exception) { process.destroyForcibly() - process.waitFor(30, TimeUnit.SECONDS) + process.waitFor(30, SECONDS) throw e } } @@ -59,6 +59,8 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension executor.submit { val exitValue = connector.waitFor() log.info("Terminal has exited (value={})", exitValue) + // TODO: Remove this arbitrary sleep when https://github.com/corda/corda/issues/689 is fixed. + try { Thread.sleep(SECONDS.toMillis(2)) } catch (e: InterruptedException) {} onExit(exitValue) } @@ -66,6 +68,7 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension session.start() } + @Suppress("unused") @Throws(InterruptedException::class) fun waitFor(): Int? = terminal.ttyConnector?.waitFor() diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt index 43ec90a4cd..e3d1f95c13 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/rpc/NodeRPC.kt @@ -17,8 +17,8 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo } private val rpcClient = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort)) + private var rpcConnection: CordaRPCConnection? = null private val timer = Timer() - private val connections = Collections.synchronizedCollection(ArrayList()) init { val setupTask = object : TimerTask() { @@ -26,7 +26,7 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo try { val user = config.users.elementAt(0) val connection = rpcClient.start(user.username, user.password) - connections.add(connection) + rpcConnection = connection val ops = connection.proxy // Cancel the "setup" task now that we've created the RPC client. @@ -53,7 +53,11 @@ class NodeRPC(config: NodeConfig, start: (NodeConfig, CordaRPCOps) -> Unit, invo override fun close() { timer.cancel() - connections.forEach(CordaRPCConnection::close) + try { + rpcConnection?.close() + } catch (e: Exception) { + log.error("Failed to close RPC connection (Error: {})", e.message) + } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt index 2c87ab73c1..44a62537cb 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTabView.kt @@ -14,6 +14,7 @@ import javafx.scene.layout.Pane import javafx.scene.layout.Priority import javafx.stage.FileChooser import javafx.util.StringConverter +import net.corda.core.crypto.commonName import net.corda.core.div import net.corda.core.exists import net.corda.core.node.CityDatabase @@ -24,7 +25,6 @@ import net.corda.core.utilities.validateLegalName import net.corda.core.writeLines import net.corda.demobench.model.* import net.corda.demobench.ui.CloseableTab -import org.bouncycastle.asn1.x500.style.RFC4519Style.name import org.controlsfx.control.CheckListView import tornadofx.* import java.nio.file.Path @@ -41,7 +41,7 @@ class NodeTabView : Fragment() { const val textWidth = 465.0 val jvm by inject() - val cordappPathsFile = jvm.dataHome / "cordapp-paths.txt" + val cordappPathsFile: Path = jvm.dataHome / "cordapp-paths.txt" fun loadDefaultCordappPaths(): MutableList { if (cordappPathsFile.exists()) @@ -267,11 +267,16 @@ class NodeTabView : Fragment() { if (countryCode != null) { nodeTab.graphic = ImageView(flags.get()[countryCode]).apply { fitWidth = 24.0; isPreserveRatio = true } } - nodeTab.text = config.legalName.toString() + nodeTab.text = config.legalName.commonName nodeTerminalView.open(config) { exitCode -> Platform.runLater { - if (exitCode == 0) + if (exitCode == 0) { nodeTab.requestClose() + } else { + // The node did not shut down cleanly. Keep the + // terminal open but ensure that it is disabled. + nodeTerminalView.shutdown() + } nodeController.dispose(config) main.forceAtLeastOneTab() } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt index cd37b5f48c..7a44b60604 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/views/NodeTerminalView.kt @@ -17,6 +17,7 @@ import javafx.scene.layout.StackPane import javafx.scene.layout.HBox import javafx.scene.layout.VBox import javafx.util.Duration +import net.corda.core.crypto.commonName import net.corda.core.failure import net.corda.core.success import net.corda.core.then @@ -64,7 +65,7 @@ class NodeTerminalView : Fragment() { private lateinit var swingTerminal: SwingNode fun open(config: NodeConfig, onExit: (Int) -> Unit) { - nodeName.text = config.legalName.toString() + nodeName.text = config.legalName.commonName swingTerminal = SwingNode() swingTerminal.setOnMouseClicked { @@ -228,16 +229,21 @@ class NodeTerminalView : Fragment() { } } + fun shutdown() { + header.isDisable = true + subscriptions.forEach { + // Don't allow any exceptions here to halt tab destruction. + try { it.unsubscribe() } catch (e: Exception) {} + } + webServer.close() + explorer.close() + viewer.close() + rpc?.close() + } + fun destroy() { if (!isDestroyed) { - subscriptions.forEach { - // Don't allow any exceptions here to halt tab destruction. - try { it.unsubscribe() } catch (e: Exception) {} - } - webServer.close() - explorer.close() - viewer.close() - rpc?.close() + shutdown() pty?.close() isDestroyed = true } diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt index 5f850af98e..bf7f7e57a3 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeControllerTest.kt @@ -1,9 +1,8 @@ package net.corda.demobench.model -import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.X509Utilities.getX509Name import net.corda.core.utilities.DUMMY_NOTARY import net.corda.nodeapi.User -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.nio.file.Path import java.nio.file.Paths @@ -13,13 +12,13 @@ class NodeControllerTest { private val baseDir: Path = Paths.get(".").toAbsolutePath() private val controller = NodeController({ _, _ -> }) - private val node1Name = X500Name("CN=Node 1,OU=Corda QA Department,O=R3 CEV,L=New York,C=US") - private val node2Name = X500Name("CN=Node 2,OU=Corda QA Department,O=R3 CEV,L=New York,C=US") + private val node1Name = "Node 1" + private val node2Name = "Node 2" @Test fun `test unique nodes after validate`() { val data = NodeData() - data.legalName.value = node1Name.toString() + data.legalName.value = node1Name assertNotNull(controller.validate(data)) assertNull(controller.validate(data)) } @@ -27,7 +26,7 @@ class NodeControllerTest { @Test fun `test unique key after validate`() { val data = NodeData() - data.legalName.value = node1Name.toString() + data.legalName.value = node1Name assertFalse(controller.keyExists("node1")) controller.validate(data) @@ -37,7 +36,7 @@ class NodeControllerTest { @Test fun `test matching name after validate`() { val data = NodeData() - data.legalName.value = node1Name.toString() + data.legalName.value = node1Name assertFalse(controller.nameExists("Node 1")) assertFalse(controller.nameExists("Node1")) @@ -51,7 +50,7 @@ class NodeControllerTest { @Test fun `test first validated node becomes network map`() { val data = NodeData() - data.legalName.value = node1Name.toString() + data.legalName.value = node1Name data.p2pPort.value = 100000 assertFalse(controller.hasNetworkMap()) @@ -61,14 +60,14 @@ class NodeControllerTest { @Test fun `test register unique nodes`() { - val config = createConfig(legalName = node2Name) + val config = createConfig(commonName = node2Name) assertTrue(controller.register(config)) assertFalse(controller.register(config)) } @Test fun `test unique key after register`() { - val config = createConfig(legalName = node2Name) + val config = createConfig(commonName = node2Name) assertFalse(controller.keyExists("node2")) controller.register(config) @@ -77,7 +76,7 @@ class NodeControllerTest { @Test fun `test matching name after register`() { - val config = createConfig(legalName = node2Name) + val config = createConfig(commonName = node2Name) assertFalse(controller.nameExists("Node 2")) assertFalse(controller.nameExists("Node2")) @@ -90,7 +89,7 @@ class NodeControllerTest { @Test fun `test register network map node`() { - val config = createConfig(legalName = X500Name("CN=Node is Network Map,OU=Corda QA Department,O=R3 CEV,L=New York,C=US")) + val config = createConfig(commonName = "Node is Network Map") assertTrue(config.isNetworkMap()) assertFalse(controller.hasNetworkMap()) @@ -100,7 +99,7 @@ class NodeControllerTest { @Test fun `test register non-network-map node`() { - val config = createConfig(legalName = X500Name("CN=Node is not Network Map,OU=Corda QA Department,O=R3 CEV,L=New York,C=US")) + val config = createConfig(commonName = "Node is not Network Map") config.networkMap = NetworkMapConfig(DUMMY_NOTARY.name, 10000) assertFalse(config.isNetworkMap()) @@ -155,7 +154,7 @@ class NodeControllerTest { @Test fun `dispose node`() { - val config = createConfig(legalName = X500Name("CN=MyName,OU=Corda QA Department,O=R3 CEV,L=New York,C=US")) + val config = createConfig(commonName = "MyName") controller.register(config) assertEquals(NodeState.STARTING, config.state) @@ -166,7 +165,7 @@ class NodeControllerTest { } private fun createConfig( - legalName: X500Name = X509Utilities.getDevX509Name("Unknown"), + commonName: String = "Unknown", p2pPort: Int = -1, rpcPort: Int = -1, webPort: Int = -1, @@ -175,7 +174,12 @@ class NodeControllerTest { users: List = listOf(user("guest")) ) = NodeConfig( baseDir, - legalName = legalName, + legalName = getX509Name( + myLegalName = commonName, + nearestCity = "New York", + country = "US", + email = "corda@city.us.example" + ), p2pPort = p2pPort, rpcPort = rpcPort, webPort = webPort, diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index fb89bc99f5..f3609ec2db 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -33,6 +33,10 @@ dependencies { compile project(':node') compile project(':finance') + // Capsule is a library for building independently executable fat JARs. + // We only need this dependency to compile our Caplet against. + compileOnly "co.paralleluniverse:capsule:$capsule_version" + // FontAwesomeFX: The "FontAwesome" icon library. compile 'de.jensd:fontawesomefx-fontawesome:4.7.0' diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 29e85a49c8..9c4c929e1a 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -25,14 +25,14 @@ repositories { sourceCompatibility = 1.6 targetCompatibility = 1.6 -dependencies { - compile project(':tools:explorer') -} - -task buildExplorerJAR(type: FatCapsule) { +task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').compileJava) { applicationClass 'net.corda.explorer.Main' archiveName "node-explorer-${corda_release_version}.jar" - applicationSource = files(project.tasks.findByName('jar'), '../build/classes/main/ExplorerCaplet.class') + applicationSource = files( + project(':tools:explorer').configurations.compile, + project(':tools:explorer').jar, + '../build/classes/main/ExplorerCaplet.class' + ) classifier 'fat' capsuleManifest { diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt index a956b864a4..0dbc6b5e4e 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt @@ -1,14 +1,10 @@ package net.corda.explorer.plugin -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.OpaqueBytes import net.corda.flows.IssuerFlow import java.util.function.Function class ExplorerPlugin : CordaPluginRegistry() { - // A list of flow that are required for this cordapp - override val requiredFlows: Map> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt index 876e217e7c..73e7aeefa2 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/GuiUtilities.kt @@ -15,7 +15,7 @@ import net.corda.client.jfx.model.NetworkIdentityModel import net.corda.client.jfx.utils.map import net.corda.contracts.asset.Cash import net.corda.core.contracts.StateAndRef -import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import tornadofx.* @@ -91,4 +91,4 @@ fun Collection.cross(other: Collection) = this.flatMap { a -> other // TODO: This is a temporary fix for the UI to show the correct issuer identity, this will break when we start randomizing keys. More work is needed here when the identity work is done. fun StateAndRef.resolveIssuer(): ObservableValue = state.data.amount.token.issuer.party.resolveIssuer() -fun AnonymousParty.resolveIssuer(): ObservableValue = Models.get(NetworkIdentityModel::class, javaClass.kotlin).lookup(owningKey).map { it?.legalIdentity } +fun AbstractParty.resolveIssuer(): ObservableValue = Models.get(NetworkIdentityModel::class, javaClass.kotlin).lookup(owningKey).map { it?.legalIdentity } diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt index 9c705e79ff..a72f452a59 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/Network.kt @@ -170,7 +170,7 @@ class Network : CordaView() { return Point2D(x, y) } - private fun List.getParties() = map { it.participants.map { getModel().lookup(it) } }.flatten() + private fun List.getParties() = map { it.participants.map { getModel().lookup(it.owningKey) } }.flatten() private fun fireBulletBetweenNodes(senderNode: Party, destNode: Party, startType: String, endType: String) { allComponentMap[senderNode]?.let { senderNode -> diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt index 4eb6acb584..71416b17e6 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/TransactionViewer.kt @@ -175,7 +175,7 @@ class TransactionViewer : CordaView("Transactions") { }.filterNotNull().toSet().joinToString(separator) } - private fun ObservableList>.getParties() = map { it.state.data.participants.map { getModel().lookup(it) } } + private fun ObservableList>.getParties() = map { it.state.data.participants.map { getModel().lookup(it.owningKey) } } private fun ObservableList>.toText() = map { it.contract().javaClass.simpleName }.groupBy { it }.map { "${it.key} (${it.value.size})" }.joinToString() private class TransactionWidget : BorderPane() { @@ -243,7 +243,7 @@ class TransactionViewer : CordaView("Transactions") { } row { label("Issuer :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } - val anonymousIssuer: AnonymousParty = data.amount.token.issuer.party + val anonymousIssuer: AbstractParty = data.amount.token.issuer.party val issuer: AbstractParty = anonymousIssuer.resolveIssuer().value ?: anonymousIssuer // TODO: Anonymous should probably be italicised or similar label(issuer.nameOrNull()?.let { PartyNameFormatter.short.format(it) } ?: "Anonymous") { @@ -253,9 +253,9 @@ class TransactionViewer : CordaView("Transactions") { row { label("Owner :") { gridpaneConstraints { hAlignment = HPos.RIGHT } } val owner = data.owner - val nodeInfo = getModel().lookup(owner) + val nodeInfo = getModel().lookup(owner.owningKey) label(nodeInfo.map { it?.legalIdentity?.let { PartyNameFormatter.short.format(it.name) } ?: "???" }) { - tooltip(data.owner.toBase58String()) + tooltip(data.owner.owningKey.toBase58String()) } } } @@ -278,17 +278,17 @@ private fun calculateTotalEquiv(identity: NodeInfo?, inputs: List, outputs: List): AmountDiff { val (reportingCurrency, exchange) = reportingCurrencyExchange - val publicKey = identity?.legalIdentity?.owningKey + val legalIdentity = identity?.legalIdentity fun List.sum() = this.map { it as? Cash.State } .filterNotNull() - .filter { publicKey == it.owner } + .filter { legalIdentity == it.owner } .map { exchange(it.amount.withoutIssuer()).quantity } .sum() // For issuing cash, if I am the issuer and not the owner (e.g. issuing cash to other party), count it as negative. val issuedAmount = if (inputs.isEmpty()) outputs.map { it as? Cash.State } .filterNotNull() - .filter { publicKey == it.amount.token.issuer.party.owningKey && publicKey != it.owner } + .filter { legalIdentity == it.amount.token.issuer.party && legalIdentity != it.owner } .map { exchange(it.amount.withoutIssuer()).quantity } .sum() else 0 diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt index 48b9b2a696..0d4f8f16d7 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/CashViewer.kt @@ -23,8 +23,8 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Amount import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.withoutIssuer -import net.corda.core.identity.AbstractParty import net.corda.core.crypto.commonName +import net.corda.core.identity.AbstractParty import net.corda.explorer.formatters.AmountFormatter import net.corda.explorer.formatters.PartyNameFormatter import net.corda.explorer.identicon.identicon diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt index f188407b13..dba8704cd6 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/CrossCashTest.kt @@ -105,7 +105,7 @@ data class CrossCashState( it.value.map { val notifier = it.key " $notifier: [" + it.value.map { - Issued(PartyAndReference(it.first.toAnonymous(), OpaqueBytes.of(0)), it.second) + Issued(PartyAndReference(it.first, OpaqueBytes.of(0)), it.second) }.joinToString(",") + "]" }.joinToString("\n") }.joinToString("\n") diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt index ffbb91656c..435809b20b 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/NotaryTest.kt @@ -29,7 +29,7 @@ val dummyNotarisationTest = LoadTest( signWith(DUMMY_CASH_ISSUER_KEY) } val asset = issueTx.toWireTransaction().outRef(0) - val moveTx = DummyContract.move(asset, DUMMY_CASH_ISSUER.party.owningKey).apply { + val moveTx = DummyContract.move(asset, DUMMY_CASH_ISSUER.party).apply { signWith(DUMMY_CASH_ISSUER_KEY) } NotariseCommand(issueTx.toSignedTransaction(false), moveTx.toSignedTransaction(false), node) diff --git a/verifier/build.gradle b/verifier/build.gradle index ea3518a7f2..81747b43c3 100644 --- a/verifier/build.gradle +++ b/verifier/build.gradle @@ -52,6 +52,7 @@ task standaloneJar(type: Jar) { from { configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } } + with jar exclude("META-INF/*.DSA") exclude("META-INF/*.RSA") exclude("META-INF/*.SF") @@ -65,3 +66,5 @@ task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath } + +build.dependsOn standaloneJar diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt index 42aa9c5488..78a84a497d 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/GeneratedLedger.kt @@ -3,6 +3,8 @@ package net.corda.verifier import net.corda.client.mock.* import net.corda.core.contracts.* import net.corda.core.crypto.* +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.WireTransaction @@ -135,7 +137,7 @@ data class GeneratedLedger( val newNotaryGen = pickOneOrMaybeNew(identities - inputNotary, partyGenerator) val inputsGen = Generator.sampleBernoulli(inputsToChooseFrom) return inputsGen.bind { inputs -> - val signers = inputs.flatMap { it.state.data.participants } + inputNotary.owningKey + val signers: List = (inputs.flatMap { it.state.data.participants } + inputNotary).map { it.owningKey } val outputsGen = Generator.sequence(inputs.map { input -> newNotaryGen.map { TransactionState(input.state.data, it, null) } }) outputsGen.combine(attachmentsGenerator) { outputs, txAttachments -> val newNotaries = outputs.map { it.notary } @@ -185,7 +187,7 @@ data class GeneratedLedger( data class GeneratedState( val nonce: Long, - override val participants: List + override val participants: List ) : ContractState { override val contract = DummyContract() } @@ -205,7 +207,7 @@ val keyPairGenerator = Generator.long().map { entropyToKeyPair(BigInteger.valueO val publicKeyGenerator = keyPairGenerator.map { it.public } val stateGenerator: Generator = Generator.replicatePoisson(2.0, publicKeyGenerator).combine(Generator.long()) { participants, nonce -> - GeneratedState(nonce, participants) + GeneratedState(nonce, participants.map { AnonymousParty(it) }) } fun commandGenerator(partiesToPickFrom: Collection): Generator> { diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt index d9e5fd3dd2..96032a9df5 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -78,7 +78,7 @@ fun verifierDriver( debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), systemProperties: Map = emptyMap(), useTestClock: Boolean = false, - automaticallyStartNetworkMap: Boolean = true, + networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), dsl: VerifierExposedDSLInterface.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( @@ -88,7 +88,7 @@ fun verifierDriver( systemProperties = systemProperties, driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, - automaticallyStartNetworkMap = automaticallyStartNetworkMap, + networkMapStartStrategy = networkMapStartStrategy, isDebug = isDebug ) ), diff --git a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt index 8531e2f53d..7000c6b7c1 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -13,6 +13,7 @@ import net.corda.core.utilities.ALICE import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow +import net.corda.node.driver.NetworkMapStartStrategy import net.corda.node.services.config.VerifierType import net.corda.node.services.transactions.ValidatingNotaryService import org.junit.Test @@ -34,7 +35,7 @@ class VerifierTests { @Test fun `single verifier works with requestor`() { - verifierDriver(automaticallyStartNetworkMap = false) { + verifierDriver { val aliceFuture = startVerificationRequestor(ALICE.name) val transactions = generateTransactions(100) val alice = aliceFuture.get() @@ -51,7 +52,7 @@ class VerifierTests { @Test fun `multiple verifiers work with requestor`() { - verifierDriver(automaticallyStartNetworkMap = false) { + verifierDriver { val aliceFuture = startVerificationRequestor(ALICE.name) val transactions = generateTransactions(100) val alice = aliceFuture.get() @@ -71,7 +72,7 @@ class VerifierTests { @Test fun `verification redistributes on verifier death`() { - verifierDriver(automaticallyStartNetworkMap = false) { + verifierDriver { val aliceFuture = startVerificationRequestor(ALICE.name) val numberOfTransactions = 100 val transactions = generateTransactions(numberOfTransactions) @@ -99,7 +100,7 @@ class VerifierTests { @Test fun `verification request waits until verifier comes online`() { - verifierDriver(automaticallyStartNetworkMap = false) { + verifierDriver { val aliceFuture = startVerificationRequestor(ALICE.name) val transactions = generateTransactions(100) val alice = aliceFuture.get() @@ -111,7 +112,7 @@ class VerifierTests { @Test fun `single verifier works with a node`() { - verifierDriver { + verifierDriver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true)) { val aliceFuture = startNode(ALICE.name) val notaryFuture = startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(ValidatingNotaryService.type)), verifierType = VerifierType.OutOfProcess) val alice = aliceFuture.get() diff --git a/verify-enclave/src/integration-test/kotlin/com/r3/enclaves/verify/NativeSgxApiTest.kt b/verify-enclave/src/integration-test/kotlin/com/r3/enclaves/verify/NativeSgxApiTest.kt index d76b1f2b62..103cd33eec 100644 --- a/verify-enclave/src/integration-test/kotlin/com/r3/enclaves/verify/NativeSgxApiTest.kt +++ b/verify-enclave/src/integration-test/kotlin/com/r3/enclaves/verify/NativeSgxApiTest.kt @@ -6,6 +6,7 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.POUNDS import net.corda.core.contracts.`issued by` +import net.corda.core.identity.AnonymousParty import net.corda.core.random63BitValue import net.corda.core.serialization.serialize import net.corda.testing.MEGA_CORP_PUBKEY @@ -27,19 +28,19 @@ class NativeSgxApiTest { ledger { // Issue a couple of cash states and spend them. val wtx1 = transaction { - output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx2 = transaction { - output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx3 = transaction { input("c1") input("c2") - output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, MINI_CORP_PUBKEY)) + output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) verifies() } diff --git a/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt b/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt index 671730b65b..5429c535eb 100644 --- a/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt +++ b/verify-enclave/src/main/kotlin/com/r3/enclaves/txverify/Enclavelet.kt @@ -8,6 +8,7 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionState import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServicesForResolution @@ -24,6 +25,8 @@ import java.io.File import java.io.InputStream import java.nio.file.Path import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate // This file implements the functionality of the SGX transaction verification enclave. @@ -49,12 +52,16 @@ private class ServicesForVerification(dependenciesList: List, a } // Identities: this stuff will all change in future so we don't bother implementing it now. + override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) = TODO("not implemented") + override fun registerIdentity(party: net.corda.core.identity.Party) = TODO("not implemented") + override fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) = TODO("not implemented") override fun partyFromKey(key: PublicKey): net.corda.core.identity.Party? = null override fun partyFromName(name: String): net.corda.core.identity.Party? = null override fun partyFromX500Name(principal: X500Name): net.corda.core.identity.Party? = null - override fun partyFromAnonymous(party: AnonymousParty): net.corda.core.identity.Party? = null + override fun partyFromAnonymous(party: AbstractParty): Party? = null + override fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? = null // TODO: Implement attachments. override val attachments: AttachmentStorage = this diff --git a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt index f713d506b5..5f7ca36bdf 100644 --- a/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt +++ b/verify-enclave/src/test/kotlin/com/r3/enclaves/txverify/EnclaveletTest.kt @@ -4,6 +4,7 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.core.contracts.POUNDS import net.corda.core.contracts.`issued by` +import net.corda.core.identity.AnonymousParty import net.corda.core.random63BitValue import net.corda.core.serialization.serialize import net.corda.testing.MEGA_CORP_PUBKEY @@ -20,19 +21,19 @@ class EnclaveletTest { ledger { // Issue a couple of cash states and spend them. val wtx1 = transaction { - output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx2 = transaction { - output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx3 = transaction { input("c1") input("c2") - output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, MINI_CORP_PUBKEY)) + output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) command(MEGA_CORP_PUBKEY, Cash.Commands.Move()) verifies() } @@ -49,19 +50,19 @@ class EnclaveletTest { ledger { // Issue a couple of cash states and spend them. val wtx1 = transaction { - output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c1", Cash.State(1000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx2 = transaction { - output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, MEGA_CORP_PUBKEY)) + output("c2", Cash.State(2000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MEGA_CORP_PUBKEY))) command(DUMMY_CASH_ISSUER.party.owningKey, Cash.Commands.Issue(random63BitValue())) verifies() } val wtx3 = transaction { input("c1") input("c2") - output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, MINI_CORP_PUBKEY)) + output(Cash.State(3000.POUNDS `issued by` DUMMY_CASH_ISSUER, AnonymousParty(MINI_CORP_PUBKEY))) failsWith("Required net.corda.core.contracts.FungibleAsset.Commands.Move command") } val req = TransactionVerificationRequest(wtx3.serialized, arrayOf(wtx1.serialized, wtx2.serialized), emptyArray()) diff --git a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt index 38bcd8a427..8de361d8a1 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -30,14 +30,6 @@ class CorDappInfoServlet(val plugins: List, val rpc: CordaR } else { plugins.forEach { plugin -> h3 { +plugin::class.java.name } - if (plugin.requiredFlows.isNotEmpty()) { - div { - p { +"Whitelisted flows:" } - ul { - plugin.requiredFlows.map { it.key }.forEach { li { +it } } - } - } - } if (plugin.webApis.isNotEmpty()) { div { plugin.webApis.forEach { api -> @@ -56,7 +48,7 @@ class CorDappInfoServlet(val plugins: List, val rpc: CordaR div { p { +"Static web content:" } ul { - plugin.staticServeDirs.map { it.key }.forEach { + plugin.staticServeDirs.keys.forEach { li { a("web/$it") { +it } } } } diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 1a7bf04992..55844b5ff6 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -19,17 +19,14 @@ configurations { sourceCompatibility = 1.6 targetCompatibility = 1.6 -dependencies { - compile project(':webserver') -} - -task buildWebserverJar(type: FatCapsule) { +task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationClass 'net.corda.webserver.WebServer' archiveName "corda-webserver-${corda_release_version}.jar" applicationSource = files( - project.tasks.findByName('jar'), - new File(project(':node').rootDir, 'node/build/classes/main/CordaCaplet.class'), - new File(project(':node').rootDir, 'node/build/classes/main/CordaCaplet$1.class'), + project(':webserver').configurations.compile, + project(':webserver').jar, + new File(project(':node').buildDir, 'classes/main/CordaCaplet.class'), + new File(project(':node').buildDir, 'classes/main/CordaCaplet$1.class'), "$rootDir/config/dev/log4j2.xml" ) from 'NOTICE' // Copy CDDL notice

@@ -86,7 +87,7 @@ open class NetClause : Clause( } // TODO: Handle proxies nominated by parties, i.e. a central clearing service - val involvedParties = inputs.map { it.beneficiary }.union(inputs.map { it.obligor.owningKey }).toSet() + val involvedParties: Set = inputs.map { it.beneficiary.owningKey }.union(inputs.map { it.obligor.owningKey }).toSet() when (command.value.type) { // For close-out netting, allow any involved party to sign NetType.CLOSE_OUT -> require(command.signers.intersect(involvedParties).isNotEmpty()) { "any involved party has signed" } diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt b/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt index a32fe333f1..5e21ed316b 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt @@ -5,6 +5,7 @@ import net.corda.core.contracts.DealState import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.UniqueIdentifier import net.corda.core.crypto.* +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder @@ -17,12 +18,12 @@ class DummyDealContract : Contract { data class State( override val contract: Contract = DummyDealContract(), - override val participants: List = listOf(), + override val participants: List = listOf(), override val linearId: UniqueIdentifier = UniqueIdentifier(), override val ref: String, override val parties: List = listOf()) : DealState { override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.containsAny(ourKeys) } + return participants.any { it.owningKey.containsAny(ourKeys) } } override fun generateAgreement(notary: Party): TransactionBuilder { diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt b/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt index 3671bf637d..6809dec4fd 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/DummyLinearContract.kt @@ -4,9 +4,9 @@ import net.corda.core.contracts.* import net.corda.core.contracts.clauses.Clause import net.corda.core.contracts.clauses.FilterOn import net.corda.core.contracts.clauses.verifyClause -import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.containsAny +import net.corda.core.identity.AbstractParty import java.security.PublicKey class DummyLinearContract : Contract { @@ -20,11 +20,11 @@ class DummyLinearContract : Contract { data class State( override val linearId: UniqueIdentifier = UniqueIdentifier(), override val contract: Contract = DummyLinearContract(), - override val participants: List = listOf(), + override val participants: List = listOf(), val nonce: SecureHash = SecureHash.randomSHA256()) : LinearState { override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.containsAny(ourKeys) } + return participants.any { it.owningKey.containsAny(ourKeys) } } } } \ No newline at end of file diff --git a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt index 89efaa400d..926b3e7087 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt @@ -6,7 +6,8 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY import net.corda.core.contracts.* -import net.corda.core.crypto.CompositeKey +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.node.services.Vault @@ -21,17 +22,17 @@ import java.util.* @JvmOverloads fun ServiceHub.fillWithSomeTestDeals(dealIds: List, revisions: Int? = 0, - participants: List = emptyList()) : Vault { + participants: List = emptyList()) : Vault { val freshKey = keyManagementService.freshKey() + val recipient = AnonymousParty(freshKey) val transactions: List = dealIds.map { // Issue a deal state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(freshKey.public))) - signWith(freshKey) + addOutputState(DummyDealContract.State(ref = it, participants = participants.plus(recipient))) signWith(DUMMY_NOTARY_KEY) } - return@map dummyIssue.toSignedTransaction() + return@map signInitialTransaction(dummyIssue) } recordTransactions(transactions) @@ -47,18 +48,18 @@ fun ServiceHub.fillWithSomeTestDeals(dealIds: List, @JvmOverloads fun ServiceHub.fillWithSomeTestLinearStates(numberToCreate: Int, uid: UniqueIdentifier = UniqueIdentifier(), - participants: List = emptyList()) : Vault { + participants: List = emptyList()) : Vault { val freshKey = keyManagementService.freshKey() + val recipient = AnonymousParty(freshKey) val transactions: List = (1..numberToCreate).map { // Issue a Linear state val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = uid, participants = participants.plus(freshKey.public))) - signWith(freshKey) + addOutputState(DummyLinearContract.State(linearId = uid, participants = participants.plus(recipient))) signWith(DUMMY_NOTARY_KEY) } - return@map dummyIssue.toSignedTransaction(true) + return@map signInitialTransaction(dummyIssue) } recordTransactions(transactions) @@ -87,18 +88,19 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, atMostThisManyStates: Int = 10, rng: Random = Random(), ref: OpaqueBytes = OpaqueBytes(ByteArray(1, { 1 })), - ownedBy: PublicKey? = null, + ownedBy: AbstractParty? = null, issuedBy: PartyAndReference = DUMMY_CASH_ISSUER, issuerKey: KeyPair = DUMMY_CASH_ISSUER_KEY): Vault { val amounts = calculateRandomlySizedAmounts(howMuch, atLeastThisManyStates, atMostThisManyStates, rng) - val myKey: PublicKey = ownedBy ?: myInfo.legalIdentity.owningKey + val myKey: PublicKey = ownedBy?.owningKey ?: myInfo.legalIdentity.owningKey + val me = AnonymousParty(myKey) // We will allocate one state to one transaction, for simplicities sake. val cash = Cash() val transactions: List = amounts.map { pennies -> val issuance = TransactionType.General.Builder(null) - cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), myKey, outputNotary) + cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), me, outputNotary) issuance.signWith(issuerKey) return@map issuance.toSignedTransaction(true) diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 5b5175bb8f..734692f751 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction @@ -20,6 +21,7 @@ import java.util.* * @param issuerRef the reference on the issued currency. Added to the node's legal identity to determine the * issuer. */ +@StartableByRPC class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, progressTracker: ProgressTracker) : AbstractCashFlow(progressTracker) { constructor(amount: Amount, issueRef: OpaqueBytes) : this(amount, issueRef, tracker()) @@ -42,9 +44,6 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog } catch (e: InsufficientBalanceException) { throw CashException("Exiting more cash than exists", e) } - progressTracker.currentStep = SIGNING_TX - val myKey = serviceHub.legalIdentityKey - builder.signWith(myKey) // Work out who the owners of the burnt states were val inputStatesNullable = serviceHub.vaultService.statesForRefs(builder.inputStates()) @@ -58,12 +57,14 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog // count as a reason to fail? val participants: Set = inputStates .filterIsInstance() - .map { serviceHub.identityService.partyFromKey(it.owner) } + .map { serviceHub.identityService.partyFromAnonymous(it.owner) } .filterNotNull() .toSet() + // Sign transaction + progressTracker.currentStep = SIGNING_TX + val tx = serviceHub.signInitialTransaction(builder) // Commit the transaction - val tx = builder.toSignedTransaction(checkSufficientSignatures = false) progressTracker.currentStep = FINALISING_TX finaliseTx(participants, tx, "Unable to notarise exit") return tx diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index c44df1e20a..d06177783f 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionType import net.corda.core.contracts.issuedBy import net.corda.core.identity.Party +import net.corda.core.flows.StartableByRPC import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder @@ -20,6 +21,7 @@ import java.util.* * @param recipient the party who should own the currency after it is issued. * @param notary the notary to set on the output states. */ +@StartableByRPC class CashIssueFlow(val amount: Amount, val issueRef: OpaqueBytes, val recipient: Party, @@ -35,11 +37,10 @@ class CashIssueFlow(val amount: Amount, progressTracker.currentStep = GENERATING_TX val builder: TransactionBuilder = TransactionType.General.Builder(notary = null) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) - Cash().generateIssue(builder, amount.issuedBy(issuer), recipient.owningKey, notary) + // TODO: Get a transaction key, don't just re-use the owning key + Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary) progressTracker.currentStep = SIGNING_TX - val myKey = serviceHub.legalIdentityKey - builder.signWith(myKey) - val tx = builder.toSignedTransaction() + val tx = serviceHub.signInitialTransaction(builder) progressTracker.currentStep = FINALISING_TX subFlow(FinalityFlow(tx)) return tx diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index ec212257ea..1e274ac1bb 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -4,13 +4,11 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.Amount import net.corda.core.contracts.InsufficientBalanceException import net.corda.core.contracts.TransactionType -import net.corda.core.crypto.expandedCompositeKeys -import net.corda.core.crypto.toStringShort +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker -import java.security.KeyPair import java.util.* /** @@ -20,6 +18,7 @@ import java.util.* * @param recipient the party to pay the currency to. * @param issuerConstraint if specified, the payment will be made using only cash issued by the given parties. */ +@StartableByRPC open class CashPaymentFlow( val amount: Amount, val recipient: Party, @@ -37,20 +36,17 @@ open class CashPaymentFlow( serviceHub.vaultService.generateSpend( builder, amount, - recipient.owningKey, + // TODO: Get a transaction key, don't just re-use the owning key + recipient, issuerConstraint) } catch (e: InsufficientBalanceException) { throw CashException("Insufficient cash for spend: ${e.message}", e) } progressTracker.currentStep = SIGNING_TX - keysForSigning.expandedCompositeKeys.forEach { - val key = serviceHub.keyManagementService.keys[it] ?: throw IllegalStateException("Could not find signing key for ${it.toStringShort()}") - builder.signWith(KeyPair(it, key)) - } + val tx = serviceHub.signInitialTransaction(spendTX, keysForSigning) progressTracker.currentStep = FINALISING_TX - val tx = spendTX.toSignedTransaction(checkSufficientSignatures = false) finaliseTx(setOf(recipient), tx, "Unable to notarise spend") return tx } diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index 3360bb5630..ab6b4a8883 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -6,6 +6,7 @@ import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party +import net.corda.core.flows.StartableByRPC import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.OpaqueBytes @@ -30,6 +31,7 @@ object IssuerFlow { * Returns the transaction created by the Issuer to move the cash to the Requester. */ @InitiatingFlow + @StartableByRPC class IssuanceRequester(val amount: Amount, val issueToParty: Party, val issueToPartyRef: OpaqueBytes, val issuerBankParty: Party) : FlowLogic() { @Suspendable diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index 068703060d..3f02121d19 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -3,9 +3,10 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.sumCashBy import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.seconds @@ -16,7 +17,6 @@ import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.trace import net.corda.core.utilities.unwrap -import java.security.KeyPair import java.security.PublicKey import java.util.* @@ -32,10 +32,12 @@ import java.util.* * 3. S signs it and commits it to the ledger, notarising it and distributing the final signed transaction back * to B. * - * Assuming no malicious termination, they both end the flow being in posession of a valid, signed transaction + * Assuming no malicious termination, they both end the flow being in possession of a valid, signed transaction * that represents an atomic asset swap. * * Note that it's the *seller* who initiates contact with the buyer, not vice-versa as you might imagine. + * + * TODO: Refactor this using the [CollectSignaturesFlow]. Note. It requires a large docsite update! */ object TwoPartyTradeFlow { // TODO: Common elements in multi-party transaction consensus and signing should be refactored into a superclass of this @@ -63,7 +65,7 @@ object TwoPartyTradeFlow { val notaryNode: NodeInfo, val assetToSell: StateAndRef, val price: Amount, - val myKeyPair: KeyPair, + val myKey: PublicKey, override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic() { companion object { @@ -97,9 +99,8 @@ object TwoPartyTradeFlow { private fun receiveAndCheckProposedTransaction(): SignedTransaction { progressTracker.currentStep = AWAITING_PROPOSAL - val myPublicKey = myKeyPair.public // Make the first message we'll send to kick off the flow. - val hello = SellerTradeInfo(assetToSell, price, myPublicKey) + val hello = SellerTradeInfo(assetToSell, price, myKey) // What we get back from the other side is a transaction that *might* be valid and acceptable to us, // but we must check it out thoroughly before we sign! val untrustedSTX = sendAndReceive(otherParty, hello) @@ -107,14 +108,14 @@ object TwoPartyTradeFlow { progressTracker.currentStep = VERIFYING return untrustedSTX.unwrap { // Check that the tx proposed by the buyer is valid. - val wtx: WireTransaction = it.verifySignatures(myPublicKey, notaryNode.notaryIdentity.owningKey) + val wtx: WireTransaction = it.verifySignatures(myKey, notaryNode.notaryIdentity.owningKey) logger.trace { "Received partially signed transaction: ${it.id}" } // Download and check all the things that this transaction depends on and verify it is contract-valid, // even though it is missing signatures. subFlow(ResolveTransactionsFlow(wtx, otherParty)) - if (wtx.outputs.map { it.data }.sumCashBy(myPublicKey).withoutIssuer() != price) + if (wtx.outputs.map { it.data }.sumCashBy(AnonymousParty(myKey)).withoutIssuer() != price) throw FlowException("Transaction is not sending us the right amount of cash") it @@ -136,7 +137,7 @@ object TwoPartyTradeFlow { open fun calculateOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { progressTracker.currentStep = SIGNING - return myKeyPair.sign(partialTX.id) + return serviceHub.createSignature(partialTX, myKey) } } @@ -202,12 +203,7 @@ object TwoPartyTradeFlow { private fun signWithOurKeys(cashSigningPubKeys: List, ptx: TransactionBuilder): SignedTransaction { // Now sign the transaction with whatever keys we need to move the cash. - for (publicKey in cashSigningPubKeys.expandedCompositeKeys) { - val privateKey = serviceHub.keyManagementService.toPrivate(publicKey) - ptx.signWith(KeyPair(publicKey, privateKey)) - } - - return ptx.toSignedTransaction(checkSufficientSignatures = false) + return serviceHub.signInitialTransaction(ptx, cashSigningPubKeys) } @Suspendable @@ -215,7 +211,7 @@ object TwoPartyTradeFlow { val ptx = TransactionType.General.Builder(notary) // Add input and output states for the movement of cash, by using the Cash contract to generate the states - val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, tradeRequest.sellerOwnerKey) + val (tx, cashSigningPubKeys) = serviceHub.vaultService.generateSpend(ptx, tradeRequest.price, AnonymousParty(tradeRequest.sellerOwnerKey)) // Add inputs/outputs/a command for the movement of the asset. tx.addInputState(tradeRequest.assetForSale) @@ -225,9 +221,9 @@ object TwoPartyTradeFlow { // reveal who the owner actually is. The key management service is expected to derive a unique key from some // initial seed in order to provide privacy protection. val freshKey = serviceHub.keyManagementService.freshKey() - val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(freshKey.public) + val (command, state) = tradeRequest.assetForSale.state.data.withNewOwner(AnonymousParty(freshKey)) tx.addOutputState(state, tradeRequest.assetForSale.state.notary) - tx.addCommand(command, tradeRequest.assetForSale.state.data.owner) + tx.addCommand(command, tradeRequest.assetForSale.state.data.owner.owningKey) // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt // to have one. diff --git a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java index 866172402c..2c8b2d4595 100644 --- a/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java +++ b/finance/src/test/java/net/corda/contracts/asset/CashTestsJava.java @@ -1,12 +1,15 @@ package net.corda.contracts.asset; -import kotlin.*; -import net.corda.core.contracts.*; -import net.corda.core.serialization.*; -import org.junit.*; +import kotlin.Unit; +import net.corda.core.contracts.PartyAndReference; +import net.corda.core.identity.AnonymousParty; +import net.corda.core.serialization.OpaqueBytes; +import org.junit.Test; -import static net.corda.core.contracts.ContractsDSL.*; -import static net.corda.core.utilities.TestConstants.*; +import static net.corda.core.contracts.ContractsDSL.DOLLARS; +import static net.corda.core.contracts.ContractsDSL.issuedBy; +import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_1; +import static net.corda.core.utilities.TestConstants.getDUMMY_PUBKEY_2; import static net.corda.testing.CoreTestUtils.*; /** @@ -15,8 +18,8 @@ import static net.corda.testing.CoreTestUtils.*; public class CashTestsJava { private final OpaqueBytes defaultRef = new OpaqueBytes(new byte[]{1}); private final PartyAndReference defaultIssuer = getMEGA_CORP().ref(defaultRef); - private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), getDUMMY_PUBKEY_1()); - private final Cash.State outState = new Cash.State(inState.getAmount(), getDUMMY_PUBKEY_2()); + private final Cash.State inState = new Cash.State(issuedBy(DOLLARS(1000), defaultIssuer), new AnonymousParty(getDUMMY_PUBKEY_1())); + private final Cash.State outState = new Cash.State(inState.getAmount(), new AnonymousParty(getDUMMY_PUBKEY_2())); @Test public void trivial() { @@ -26,7 +29,7 @@ public class CashTestsJava { tx.failsWith("the amounts balance"); tx.tweak(tw -> { - tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), getDUMMY_PUBKEY_2())); + tw.output(new Cash.State(issuedBy(DOLLARS(2000), defaultIssuer), new AnonymousParty(getDUMMY_PUBKEY_2()))); return tw.failsWith("the amounts balance"); }); diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index b68aaf0221..11f47d4161 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -4,15 +4,13 @@ import net.corda.contracts.asset.* import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* import net.corda.core.days +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.core.utilities.TEST_TX_TIME +import net.corda.core.utilities.* import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import net.corda.testing.* @@ -38,7 +36,7 @@ interface ICommercialPaperTestTemplate { class JavaCommercialPaperTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = JavaCommercialPaper.State( MEGA_CORP.ref(123), - MEGA_CORP_PUBKEY, + MEGA_CORP, 1000.DOLLARS `issued by` MEGA_CORP.ref(123), TEST_TX_TIME + 7.days ) @@ -51,7 +49,7 @@ class JavaCommercialPaperTest : ICommercialPaperTestTemplate { class KotlinCommercialPaperTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = CommercialPaper.State( issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP_PUBKEY, + owner = MEGA_CORP, faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), maturityDate = TEST_TX_TIME + 7.days ) @@ -64,7 +62,7 @@ class KotlinCommercialPaperTest : ICommercialPaperTestTemplate { class KotlinCommercialPaperLegacyTest : ICommercialPaperTestTemplate { override fun getPaper(): ICommercialPaperState = CommercialPaperLegacy.State( issuance = MEGA_CORP.ref(123), - owner = MEGA_CORP_PUBKEY, + owner = MEGA_CORP, faceValue = 1000.DOLLARS `issued by` MEGA_CORP.ref(123), maturityDate = TEST_TX_TIME + 7.days ) @@ -91,8 +89,8 @@ class CommercialPaperTestsGeneric { val someProfits = 1200.DOLLARS `issued by` issuer ledger { unverifiedTransaction { - output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE_PUBKEY) - output("some profits", someProfits.STATE `owned by` MEGA_CORP_PUBKEY) + output("alice's $900", 900.DOLLARS.CASH `issued by` issuer `owned by` ALICE) + output("some profits", someProfits.STATE `owned by` MEGA_CORP) } // Some CP is issued onto the ledger by MegaCorp. @@ -108,8 +106,8 @@ class CommercialPaperTestsGeneric { transaction("Trade") { input("paper") input("alice's $900") - output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP_PUBKEY } - output("alice's paper") { "paper".output() `owned by` ALICE_PUBKEY } + output("borrowed $900") { 900.DOLLARS.CASH `issued by` issuer `owned by` MEGA_CORP } + output("alice's paper") { "paper".output() `owned by` ALICE } command(ALICE_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { thisTest.getMoveCommand() } this.verifies() @@ -122,8 +120,8 @@ class CommercialPaperTestsGeneric { input("some profits") fun TransactionDSL.outputs(aliceGetsBack: Amount>) { - output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE_PUBKEY } - output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP_PUBKEY } + output("Alice's profit") { aliceGetsBack.STATE `owned by` ALICE } + output("Change") { (someProfits - aliceGetsBack).STATE `owned by` MEGA_CORP } } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } @@ -270,8 +268,8 @@ class CommercialPaperTestsGeneric { // Alice pays $9000 to BigCorp to own some of their debt. moveTX = run { val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - aliceVaultService.generateSpend(ptx, 9000.DOLLARS, bigCorpServices.key.public) - CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), aliceServices.key.public) + aliceVaultService.generateSpend(ptx, 9000.DOLLARS, AnonymousParty(bigCorpServices.key.public)) + CommercialPaper().generateMove(ptx, issueTX.tx.outRef(0), AnonymousParty(aliceServices.key.public)) ptx.signWith(bigCorpServices.key) ptx.signWith(aliceServices.key) ptx.signWith(DUMMY_NOTARY_KEY) diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt index 8bbfd5d5fa..bbb6e1aaf1 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -2,8 +2,10 @@ package net.corda.contracts.asset import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.generateKeyPair import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.VaultService import net.corda.core.node.services.unconsumedStates @@ -23,7 +25,6 @@ import org.junit.Before import org.junit.Test import java.io.Closeable import java.security.KeyPair -import java.security.PublicKey import java.util.* import kotlin.test.* @@ -32,11 +33,11 @@ class CashTests { val defaultIssuer = MEGA_CORP.ref(defaultRef) val inState = Cash.State( amount = 1000.DOLLARS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_1 + owner = AnonymousParty(DUMMY_PUBKEY_1) ) // Input state held by the issuer - val issuerInState = inState.copy(owner = defaultIssuer.party.owningKey) - val outState = issuerInState.copy(owner = DUMMY_PUBKEY_2) + val issuerInState = inState.copy(owner = defaultIssuer.party) + val outState = issuerInState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2)) fun Cash.State.editDepositRef(ref: Byte) = copy( amount = Amount(amount.quantity, token = amount.token.copy(amount.token.issuer.copy(reference = OpaqueBytes.of(ref)))) @@ -70,13 +71,13 @@ class CashTests { } services.fillWithSomeTestCash(howMuch = 100.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1) + issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) services.fillWithSomeTestCash(howMuch = 400.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_PUBKEY_1) + issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, ownedBy = OUR_IDENTITY_1) services.fillWithSomeTestCash(howMuch = 80.DOLLARS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1) + issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) services.fillWithSomeTestCash(howMuch = 80.SWISS_FRANCS, atLeastThisManyStates = 1, atMostThisManyStates = 1, - issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_PUBKEY_1) + issuedBy = MINI_CORP.ref(1), issuerKey = MINI_CORP_KEY, ownedBy = OUR_IDENTITY_1) vaultStatesUnconsumed = services.vaultService.unconsumedStates().toList() } @@ -142,7 +143,7 @@ class CashTests { output { Cash.State( amount = 1000.DOLLARS `issued by` MINI_CORP.ref(12, 34), - owner = DUMMY_PUBKEY_1 + owner = AnonymousParty(DUMMY_PUBKEY_1) ) } tweak { @@ -158,14 +159,14 @@ class CashTests { fun generateIssueRaw() { // Test generation works. val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction().tx assertTrue(tx.inputs.isEmpty()) val s = tx.outputs[0].data as Cash.State assertEquals(100.DOLLARS `issued by` MINI_CORP.ref(12, 34), s.amount) assertEquals(MINI_CORP as AbstractParty, s.amount.token.issuer.party) - assertEquals(DUMMY_PUBKEY_1, s.owner) + assertEquals(AnonymousParty(DUMMY_PUBKEY_1), s.owner) assertTrue(tx.commands[0].value is Cash.Commands.Issue) assertEquals(MINI_CORP_PUBKEY, tx.commands[0].signers[0]) } @@ -175,7 +176,7 @@ class CashTests { // Test issuance from an issued amount val amount = 100.DOLLARS `issued by` MINI_CORP.ref(12, 34) val tx: WireTransaction = TransactionType.General.Builder(notary = null).apply { - Cash().generateIssue(this, amount, owner = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + Cash().generateIssue(this, amount, owner = AnonymousParty(DUMMY_PUBKEY_1), notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction().tx assertTrue(tx.inputs.isEmpty()) @@ -248,14 +249,14 @@ class CashTests { // Issue some cash var ptx = TransactionType.General.Builder(DUMMY_NOTARY) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) ptx.signWith(MINI_CORP_KEY) val tx = ptx.toSignedTransaction() // Include the previously issued cash in a new issuance command ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef(0)) - Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + Cash().generateIssue(ptx, 100.DOLLARS `issued by` MINI_CORP.ref(12, 34), owner = MINI_CORP, notary = DUMMY_NOTARY) } @Test @@ -329,7 +330,7 @@ class CashTests { input { inState.copy( amount = 150.POUNDS `issued by` defaultIssuer, - owner = DUMMY_PUBKEY_2 + owner = AnonymousParty(DUMMY_PUBKEY_2) ) } output { outState.copy(amount = 1150.DOLLARS `issued by` defaultIssuer) } @@ -382,10 +383,10 @@ class CashTests { // Multi-issuer case. transaction { input { issuerInState } - input { issuerInState.copy(owner = MINI_CORP_PUBKEY) `issued by` MINI_CORP } + input { issuerInState.copy(owner = MINI_CORP) `issued by` MINI_CORP } output { issuerInState.copy(amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) `issued by` MINI_CORP } - output { issuerInState.copy(owner = MINI_CORP_PUBKEY, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) } + output { issuerInState.copy(owner = MINI_CORP, amount = issuerInState.amount - (200.DOLLARS `issued by` defaultIssuer)) } command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { Cash.Commands.Move() } @@ -420,19 +421,19 @@ class CashTests { // Can't merge them together. tweak { - output { inState.copy(owner = DUMMY_PUBKEY_2, amount = 2000.DOLLARS `issued by` defaultIssuer) } + output { inState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2), amount = 2000.DOLLARS `issued by` defaultIssuer) } this `fails with` "the amounts balance" } // Missing MiniCorp deposit tweak { - output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2) } + output { inState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2)) } + output { inState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2)) } this `fails with` "the amounts balance" } // This works. - output { inState.copy(owner = DUMMY_PUBKEY_2) } - output { inState.copy(owner = DUMMY_PUBKEY_2) `issued by` MINI_CORP } + output { inState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2)) } + output { inState.copy(owner = AnonymousParty(DUMMY_PUBKEY_2)) `issued by` MINI_CORP } command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } this.verifies() } @@ -442,11 +443,11 @@ class CashTests { fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { - val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), DUMMY_PUBKEY_2) - input { inState `owned by` DUMMY_PUBKEY_1 } + val pounds = Cash.State(658.POUNDS `issued by` MINI_CORP.ref(3, 4, 5), AnonymousParty(DUMMY_PUBKEY_2)) + input { inState `owned by` AnonymousParty(DUMMY_PUBKEY_1) } input { pounds } - output { inState `owned by` DUMMY_PUBKEY_2 } - output { pounds `owned by` DUMMY_PUBKEY_1 } + output { inState `owned by` AnonymousParty(DUMMY_PUBKEY_2) } + output { pounds `owned by` AnonymousParty(DUMMY_PUBKEY_1) } command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Cash.Commands.Move() } this.verifies() @@ -458,13 +459,13 @@ class CashTests { // Spend tx generation val OUR_KEY: KeyPair by lazy { generateKeyPair() } - val OUR_PUBKEY_1: PublicKey get() = OUR_KEY.public + val OUR_IDENTITY_1: AbstractParty get() = AnonymousParty(OUR_KEY.public) - val THEIR_PUBKEY_1 = DUMMY_PUBKEY_2 + val THEIR_IDENTITY_1 = AnonymousParty(DUMMY_PUBKEY_2) fun makeCash(amount: Amount, corp: Party, depositRef: Byte = 1) = StateAndRef( - Cash.State(amount `issued by` corp.ref(depositRef), OUR_PUBKEY_1) `with notary` DUMMY_NOTARY, + Cash.State(amount `issued by` corp.ref(depositRef), OUR_IDENTITY_1) `with notary` DUMMY_NOTARY, StateRef(SecureHash.randomSHA256(), Random().nextInt(32)) ) @@ -484,7 +485,7 @@ class CashTests { return tx.toWireTransaction() } - fun makeSpend(amount: Amount, dest: PublicKey): WireTransaction { + fun makeSpend(amount: Amount, dest: AbstractParty): WireTransaction { val tx = TransactionType.General.Builder(DUMMY_NOTARY) database.transaction { vault.generateSpend(tx, amount, dest) @@ -566,13 +567,13 @@ class CashTests { database.transaction { - val wtx = makeSpend(100.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(100.DOLLARS, THEIR_IDENTITY_1) @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) - assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data) + assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @@ -582,7 +583,7 @@ class CashTests { database.transaction { val tx = TransactionType.General.Builder(DUMMY_NOTARY) - vault.generateSpend(tx, 80.DOLLARS, ALICE_PUBKEY, setOf(MINI_CORP.toAnonymous())) + vault.generateSpend(tx, 80.DOLLARS, ALICE, setOf(MINI_CORP)) assertEquals(vaultStatesUnconsumed.elementAt(2).ref, tx.inputStates()[0]) } @@ -593,14 +594,14 @@ class CashTests { database.transaction { - val wtx = makeSpend(10.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(10.DOLLARS, THEIR_IDENTITY_1) @Suppress("UNCHECKED_CAST") val vaultState = vaultStatesUnconsumed.elementAt(0) assertEquals(vaultState.ref, wtx.inputs[0]) - assertEquals(vaultState.state.data.copy(owner = THEIR_PUBKEY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(vaultState.state.data.copy(owner = THEIR_IDENTITY_1, amount = 10.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) assertEquals(vaultState.state.data.copy(amount = 90.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @@ -608,15 +609,15 @@ class CashTests { fun generateSpendWithTwoInputs() { database.transaction { - val wtx = makeSpend(500.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(500.DOLLARS, THEIR_IDENTITY_1) @Suppress("UNCHECKED_CAST") val vaultState0 = vaultStatesUnconsumed.elementAt(0) val vaultState1 = vaultStatesUnconsumed.elementAt(1) assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) - assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[0].data) + assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @@ -624,7 +625,7 @@ class CashTests { fun generateSpendMixedDeposits() { database.transaction { - val wtx = makeSpend(580.DOLLARS, THEIR_PUBKEY_1) + val wtx = makeSpend(580.DOLLARS, THEIR_IDENTITY_1) assertEquals(3, wtx.inputs.size) @Suppress("UNCHECKED_CAST") @@ -635,9 +636,9 @@ class CashTests { assertEquals(vaultState0.ref, wtx.inputs[0]) assertEquals(vaultState1.ref, wtx.inputs[1]) assertEquals(vaultState2.ref, wtx.inputs[2]) - assertEquals(vaultState0.state.data.copy(owner = THEIR_PUBKEY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) - assertEquals(vaultState2.state.data.copy(owner = THEIR_PUBKEY_1), wtx.outputs[0].data) - assertEquals(OUR_PUBKEY_1, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) + assertEquals(vaultState0.state.data.copy(owner = THEIR_IDENTITY_1, amount = 500.DOLLARS `issued by` defaultIssuer), wtx.outputs[1].data) + assertEquals(vaultState2.state.data.copy(owner = THEIR_IDENTITY_1), wtx.outputs[0].data) + assertEquals(OUR_IDENTITY_1.owningKey, wtx.commands.single { it.value is Cash.Commands.Move }.signers[0]) } } @@ -647,12 +648,12 @@ class CashTests { database.transaction { val e: InsufficientBalanceException = assertFailsWith("balance") { - makeSpend(1000.DOLLARS, THEIR_PUBKEY_1) + makeSpend(1000.DOLLARS, THEIR_IDENTITY_1) } assertEquals((1000 - 580).DOLLARS, e.amountMissing) assertFailsWith(InsufficientBalanceException::class) { - makeSpend(81.SWISS_FRANCS, THEIR_PUBKEY_1) + makeSpend(81.SWISS_FRANCS, THEIR_IDENTITY_1) } } } @@ -662,9 +663,9 @@ class CashTests { */ @Test fun aggregation() { - val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP_PUBKEY) - val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP_PUBKEY) - val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY) + val fiveThousandDollarsFromMega = Cash.State(5000.DOLLARS `issued by` MEGA_CORP.ref(2), MEGA_CORP) + val twoThousandDollarsFromMega = Cash.State(2000.DOLLARS `issued by` MEGA_CORP.ref(2), MINI_CORP) + val oneThousandDollarsFromMini = Cash.State(1000.DOLLARS `issued by` MINI_CORP.ref(3), MEGA_CORP) // Obviously it must be possible to aggregate states with themselves assertEquals(fiveThousandDollarsFromMega.amount.token, fiveThousandDollarsFromMega.amount.token) @@ -678,7 +679,7 @@ class CashTests { // States cannot be aggregated if the currency differs assertNotEquals(oneThousandDollarsFromMini.amount.token, - Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP_PUBKEY).amount.token) + Cash.State(1000.POUNDS `issued by` MINI_CORP.ref(3), MEGA_CORP).amount.token) // States cannot be aggregated if the reference differs assertNotEquals(fiveThousandDollarsFromMega.amount.token, (fiveThousandDollarsFromMega `with deposit` defaultIssuer).amount.token) @@ -688,20 +689,20 @@ class CashTests { @Test fun `summing by owner`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MINI_CORP), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) ) - assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP_PUBKEY)) + assertEquals(6000.DOLLARS `issued by` defaultIssuer, states.sumCashBy(MEGA_CORP)) } @Test(expected = UnsupportedOperationException::class) fun `summing by owner throws`() { val states = listOf( - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) ) - states.sumCashBy(MINI_CORP_PUBKEY) + states.sumCashBy(MINI_CORP) } @Test @@ -720,9 +721,9 @@ class CashTests { @Test fun `summing a single currency`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), - Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), - Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), + Cash.State(2000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), + Cash.State(4000.DOLLARS `issued by` defaultIssuer, MEGA_CORP) ) // Test that summing everything produces the total number of dollars val expected = 7000.DOLLARS `issued by` defaultIssuer @@ -733,8 +734,8 @@ class CashTests { @Test(expected = IllegalArgumentException::class) fun `summing multiple currencies`() { val states = listOf( - Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP_PUBKEY), - Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP_PUBKEY) + Cash.State(1000.DOLLARS `issued by` defaultIssuer, MEGA_CORP), + Cash.State(4000.POUNDS `issued by` defaultIssuer, MEGA_CORP) ) // Test that summing everything fails because we're mixing units states.sumCash() @@ -748,14 +749,14 @@ class CashTests { output("MEGA_CORP cash") { Cash.State( amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), - owner = MEGA_CORP_PUBKEY + owner = MEGA_CORP ) } } transaction { input("MEGA_CORP cash") - output("MEGA_CORP cash".output().copy(owner = DUMMY_PUBKEY_1)) + output("MEGA_CORP cash".output().copy(owner = AnonymousParty(DUMMY_PUBKEY_1))) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } this.verifies() } @@ -764,7 +765,7 @@ class CashTests { transaction { input("MEGA_CORP cash") // We send it to another pubkey so that the transaction is not identical to the previous one - output("MEGA_CORP cash".output().copy(owner = ALICE_PUBKEY)) + output("MEGA_CORP cash".output().copy(owner = ALICE)) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } this.verifies() } diff --git a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt index 6e1f6514f1..c0333a2c26 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -2,13 +2,14 @@ package net.corda.contracts.asset import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* -import net.corda.core.crypto.NullPublicKey +import net.corda.core.crypto.NULL_PARTY import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.AnonymousParty import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.* import net.corda.testing.* import org.junit.Test -import java.security.PublicKey import java.time.Duration import java.time.temporal.ChronoUnit import java.util.* @@ -21,7 +22,7 @@ class ObligationTests { val defaultRef = OpaqueBytes(ByteArray(1, { 1 })) val defaultIssuer = MEGA_CORP.ref(defaultRef) val oneMillionDollars = 1000000.DOLLARS `issued by` defaultIssuer - val trustedCashContract = nonEmptySetOf(SecureHash.Companion.randomSHA256() as SecureHash) + val trustedCashContract = nonEmptySetOf(SecureHash.randomSHA256() as SecureHash) val megaIssuedDollars = nonEmptySetOf(Issued(defaultIssuer, USD)) val megaIssuedPounds = nonEmptySetOf(Issued(defaultIssuer, GBP)) val fivePm = TEST_TX_TIME.truncatedTo(ChronoUnit.DAYS).plus(17, ChronoUnit.HOURS) @@ -33,18 +34,18 @@ class ObligationTests { obligor = MEGA_CORP, template = megaCorpDollarSettlement, quantity = 1000.DOLLARS.quantity, - beneficiary = DUMMY_PUBKEY_1 + beneficiary = CHARLIE ) - val outState = inState.copy(beneficiary = DUMMY_PUBKEY_2) + val outState = inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) private fun cashObligationTestRoots( group: LedgerDSL ) = group.apply { unverifiedTransaction { - output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)) - output("Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY)) - output("MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB_PUBKEY)) - output("Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) + output("Alice's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + output("Bob's $1,000,000 obligation to Alice", oneMillionDollars.OBLIGATION between Pair(BOB, ALICE)) + output("MegaCorp's $1,000,000 obligation to Bob", oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB)) + output("Alice's $1,000,000", 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) } } @@ -71,13 +72,13 @@ class ObligationTests { tweak { output { outState } output { outState `issued by` MINI_CORP } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "at least one asset input" } // Simple reallocation works. tweak { output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this.verifies() } } @@ -98,7 +99,7 @@ class ObligationTests { // institution is allowed to issue as much cash as they want. transaction { output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Issue() } + command(CHARLIE.owningKey) { Obligation.Commands.Issue() } this `fails with` "output states are issued by a command signer" } transaction { @@ -106,7 +107,7 @@ class ObligationTests { Obligation.State( obligor = MINI_CORP, quantity = 1000.DOLLARS.quantity, - beneficiary = DUMMY_PUBKEY_1, + beneficiary = CHARLIE, template = megaCorpDollarSettlement ) } @@ -121,14 +122,14 @@ class ObligationTests { // Test generation works. val tx = TransactionType.General.Builder(notary = null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - beneficiary = DUMMY_PUBKEY_1, notary = DUMMY_NOTARY) + beneficiary = CHARLIE, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction().tx assertTrue(tx.inputs.isEmpty()) val expected = Obligation.State( obligor = MINI_CORP, quantity = 100.DOLLARS.quantity, - beneficiary = DUMMY_PUBKEY_1, + beneficiary = CHARLIE, template = megaCorpDollarSettlement ) assertEquals(tx.outputs[0].data, expected) @@ -142,7 +143,7 @@ class ObligationTests { // Move fails: not allowed to summon money. tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } @@ -199,7 +200,7 @@ class ObligationTests { // Issue some obligation val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction() @@ -207,16 +208,16 @@ class ObligationTests { val ptx = TransactionType.General.Builder(DUMMY_NOTARY) ptx.addInputState(tx.tx.outRef>(0)) Obligation().generateIssue(ptx, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP, notary = DUMMY_NOTARY) } /** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */ @Test fun `generate close-out net transaction`() { - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) + val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) + val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice) + Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction().tx @@ -226,24 +227,24 @@ class ObligationTests { /** Test generating a transaction to net two obligations of the different sizes, and confirm the balance is correct. */ @Test fun `generate close-out net transaction with remainder`() { - val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB_PUBKEY) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) + val obligationAliceToBob = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB) + val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Obligation().generateCloseOutNetting(this, ALICE_PUBKEY, obligationAliceToBob, obligationBobToAlice) + Obligation().generateCloseOutNetting(this, ALICE, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction().tx assertEquals(1, tx.outputs.size) val actual = tx.outputs[0].data - assertEquals((1000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB_PUBKEY), actual) + assertEquals((1000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(ALICE, BOB), actual) } /** Test generating a transaction to net two obligations of the same size, and therefore there are no outputs. */ @Test fun `generate payment net transaction`() { - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) - val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE_PUBKEY) + val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) + val obligationBobToAlice = oneMillionDollars.OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(DUMMY_NOTARY).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) @@ -256,8 +257,8 @@ class ObligationTests { /** Test generating a transaction to two obligations, where one is bigger than the other and therefore there is a remainder. */ @Test fun `generate payment net transaction with remainder`() { - val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) - val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE_PUBKEY) + val obligationAliceToBob = oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) + val obligationBobToAlice = (2000000.DOLLARS `issued by` defaultIssuer).OBLIGATION between Pair(BOB, ALICE) val tx = TransactionType.General.Builder(null).apply { Obligation().generatePaymentNetting(this, obligationAliceToBob.amount.token, DUMMY_NOTARY, obligationAliceToBob, obligationBobToAlice) signWith(ALICE_KEY) @@ -278,7 +279,7 @@ class ObligationTests { // Generate a transaction issuing the obligation var tx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement.copy(dueBefore = dueBefore), 100.DOLLARS.quantity, - beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction() var stateAndRef = tx.tx.outRef>(0) @@ -309,14 +310,14 @@ class ObligationTests { @Test fun `generate settlement transaction`() { val cashTx = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP_PUBKEY, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` defaultIssuer, MINI_CORP, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction().tx // Generate a transaction issuing the obligation val obligationTx = TransactionType.General.Builder(null).apply { Obligation().generateIssue(this, MINI_CORP, megaCorpDollarSettlement, 100.DOLLARS.quantity, - beneficiary = MINI_CORP_PUBKEY, notary = DUMMY_NOTARY) + beneficiary = MINI_CORP, notary = DUMMY_NOTARY) signWith(MINI_CORP_KEY) }.toSignedTransaction().tx @@ -354,7 +355,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB_PUBKEY) } + output("change") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, BOB) } command(BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } timestamp(TEST_TX_TIME) this.verifies() @@ -368,7 +369,7 @@ class ObligationTests { transaction("Issuance") { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") - output("change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB_PUBKEY) } + output("change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB) } command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } timestamp(TEST_TX_TIME) this `fails with` "amounts owed on input and output must match" @@ -422,7 +423,7 @@ class ObligationTests { transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE_PUBKEY) } + output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) } command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } timestamp(TEST_TX_TIME) this.verifies() @@ -436,7 +437,7 @@ class ObligationTests { transaction("Issuance") { input("Bob's $1,000,000 obligation to Alice") input("MegaCorp's $1,000,000 obligation to Bob") - output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE_PUBKEY) } + output("MegaCorp's $1,000,000 obligation to Alice") { oneMillionDollars.OBLIGATION between Pair(MEGA_CORP, ALICE) } command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } timestamp(TEST_TX_TIME) this `fails with` "all involved parties have signed" @@ -452,7 +453,7 @@ class ObligationTests { transaction("Settlement") { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } + output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() @@ -463,10 +464,10 @@ class ObligationTests { val halfAMillionDollars = 500000.DOLLARS `issued by` defaultIssuer ledger { transaction("Settlement") { - input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)) - input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) - output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) } - output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } + input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)) + input(500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + output("Alice's $500,000 obligation to Bob") { halfAMillionDollars.OBLIGATION between Pair(ALICE, BOB) } + output("Bob's $500,000") { 500000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this.verifies() @@ -474,12 +475,12 @@ class ObligationTests { } // Make sure we can't settle an obligation that's defaulted - val defaultedObligation: Obligation.State = (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) + val defaultedObligation: Obligation.State = (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED) ledger { transaction("Settlement") { input(defaultedObligation) // Alice's defaulted $1,000,000 obligation to Bob - input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE_PUBKEY) - output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } + input(1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` ALICE) + output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this `fails with` "all inputs are in the normal state" @@ -492,7 +493,7 @@ class ObligationTests { transaction("Settlement") { input("Alice's $1,000,000 obligation to Bob") input("Alice's $1,000,000") - output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB_PUBKEY } + output("Bob's $1,000,000") { 1000000.DOLLARS.CASH `issued by` defaultIssuer `owned by` BOB } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneMillionDollars.quantity / 2, inState.amount.token)) } command(ALICE_PUBKEY) { Cash.Commands.Move(Obligation().legalContractReference) } this `fails with` "amount in settle command" @@ -506,17 +507,17 @@ class ObligationTests { val oneUnitFcoj = Amount(1, defaultFcoj) val obligationDef = Obligation.Terms(nonEmptySetOf(CommodityContract().legalContractReference), nonEmptySetOf(defaultFcoj), TEST_TX_TIME) val oneUnitFcojObligation = Obligation.State(Obligation.Lifecycle.NORMAL, ALICE, - obligationDef, oneUnitFcoj.quantity, NullPublicKey) + obligationDef, oneUnitFcoj.quantity, NULL_PARTY) // Try settling a simple commodity obligation ledger { unverifiedTransaction { - output("Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB_PUBKEY)) - output("Alice's 1 FCOJ", CommodityContract.State(oneUnitFcoj, ALICE_PUBKEY)) + output("Alice's 1 FCOJ obligation to Bob", oneUnitFcojObligation between Pair(ALICE, BOB)) + output("Alice's 1 FCOJ", CommodityContract.State(oneUnitFcoj, ALICE)) } transaction("Settlement") { input("Alice's 1 FCOJ obligation to Bob") input("Alice's 1 FCOJ") - output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB_PUBKEY) } + output("Bob's 1 FCOJ") { CommodityContract.State(oneUnitFcoj, BOB) } command(ALICE_PUBKEY) { Obligation.Commands.Settle(Amount(oneUnitFcoj.quantity, oneUnitFcojObligation.amount.token)) } command(ALICE_PUBKEY) { CommodityContract.Commands.Move(Obligation().legalContractReference) } verifies() @@ -531,7 +532,7 @@ class ObligationTests { cashObligationTestRoots(this) transaction("Settlement") { input("Alice's $1,000,000 obligation to Bob") - output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY)).copy(lifecycle = Lifecycle.DEFAULTED) } + output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB)).copy(lifecycle = Lifecycle.DEFAULTED) } command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } this `fails with` "there is a timestamp from the authority" } @@ -541,8 +542,8 @@ class ObligationTests { val pastTestTime = TEST_TX_TIME - Duration.ofDays(7) val futureTestTime = TEST_TX_TIME + Duration.ofDays(7) transaction("Settlement") { - input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime) - output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } + input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime) + output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` futureTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } timestamp(TEST_TX_TIME) this `fails with` "the due date has passed" @@ -551,8 +552,8 @@ class ObligationTests { // Try defaulting an obligation that is now in the past ledger { transaction("Settlement") { - input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime) - output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB_PUBKEY) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } + input(oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime) + output("Alice's defaulted $1,000,000 obligation to Bob") { (oneMillionDollars.OBLIGATION between Pair(ALICE, BOB) `at` pastTestTime).copy(lifecycle = Lifecycle.DEFAULTED) } command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } timestamp(TEST_TX_TIME) this.verifies() @@ -565,7 +566,7 @@ class ObligationTests { fun testMergeSplit() { // Splitting value works. transaction { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } tweak { input { inState } repeat(4) { output { inState.copy(quantity = inState.quantity / 4) } } @@ -624,7 +625,7 @@ class ObligationTests { inState.copy( quantity = 15000, template = megaCorpPoundSettlement, - beneficiary = DUMMY_PUBKEY_2 + beneficiary = AnonymousParty(DUMMY_PUBKEY_2) ) } output { outState.copy(quantity = 115000) } @@ -635,7 +636,7 @@ class ObligationTests { input { inState } input { inState `issued by` MINI_CORP } output { outState } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } } @@ -648,17 +649,17 @@ class ObligationTests { output { outState.copy(quantity = inState.quantity - 200.DOLLARS.quantity) } tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(100.DOLLARS.quantity, inState.amount.token)) } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" } tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) } + command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token)) } this `fails with` "required net.corda.core.contracts.FungibleAsset.Commands.Move command" tweak { - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this.verifies() } } @@ -676,14 +677,14 @@ class ObligationTests { output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedPounds), quantity = inState.quantity - 200.POUNDS.quantity) } output { inState.copy(template = inState.template.copy(acceptableIssuedProducts = megaIssuedDollars), quantity = inState.quantity - 200.DOLLARS.quantity) } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this `fails with` "the amounts balance" - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) } + command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.DOLLARS.quantity, inState.amount.token.copy(product = megaCorpDollarSettlement))) } this `fails with` "the amounts balance" - command(DUMMY_PUBKEY_1) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) } + command(CHARLIE.owningKey) { Obligation.Commands.Exit(Amount(200.POUNDS.quantity, inState.amount.token.copy(product = megaCorpPoundSettlement))) } this.verifies() } } @@ -697,20 +698,20 @@ class ObligationTests { // Can't merge them together. tweak { - output { inState.copy(beneficiary = DUMMY_PUBKEY_2, quantity = 200000L) } + output { inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2), quantity = 200000L) } this `fails with` "the amounts balance" } // Missing MiniCorp deposit tweak { - output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } - output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } + output { inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) } + output { inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) } this `fails with` "the amounts balance" } // This works. - output { inState.copy(beneficiary = DUMMY_PUBKEY_2) } - output { inState.copy(beneficiary = DUMMY_PUBKEY_2) `issued by` MINI_CORP } - command(DUMMY_PUBKEY_1) { Obligation.Commands.Move() } + output { inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) } + output { inState.copy(beneficiary = AnonymousParty(DUMMY_PUBKEY_2)) `issued by` MINI_CORP } + command(CHARLIE.owningKey) { Obligation.Commands.Move() } this.verifies() } } @@ -719,12 +720,12 @@ class ObligationTests { fun multiCurrency() { // Check we can do an atomic currency trade tx. transaction { - val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, DUMMY_PUBKEY_2) - input { inState `owned by` DUMMY_PUBKEY_1 } + val pounds = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpPoundSettlement, 658.POUNDS.quantity, AnonymousParty(DUMMY_PUBKEY_2)) + input { inState `owned by` CHARLIE } input { pounds } - output { inState `owned by` DUMMY_PUBKEY_2 } - output { pounds `owned by` DUMMY_PUBKEY_1 } - command(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2) { Obligation.Commands.Move() } + output { inState `owned by` AnonymousParty(DUMMY_PUBKEY_2) } + output { pounds `owned by` CHARLIE } + command(CHARLIE.owningKey, DUMMY_PUBKEY_2) { Obligation.Commands.Move() } this.verifies() } @@ -733,11 +734,11 @@ class ObligationTests { @Test fun `nettability of settlement contracts`() { val fiveKDollarsFromMegaToMega = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, - 5000.DOLLARS.quantity, MEGA_CORP_PUBKEY) + 5000.DOLLARS.quantity, MEGA_CORP) val twoKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, - 2000.DOLLARS.quantity, MINI_CORP_PUBKEY) + 2000.DOLLARS.quantity, MINI_CORP) val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, - 1000.DOLLARS.quantity, MEGA_CORP_PUBKEY) + 1000.DOLLARS.quantity, MEGA_CORP) // Obviously states must be nettable with themselves assertEquals(fiveKDollarsFromMegaToMega.bilateralNetState, fiveKDollarsFromMegaToMega.bilateralNetState) @@ -759,7 +760,7 @@ class ObligationTests { // States must not be nettable if the cash contract differs assertNotEquals(fiveKDollarsFromMegaToMega.bilateralNetState, - fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.Companion.randomSHA256()))).bilateralNetState) + fiveKDollarsFromMegaToMega.copy(template = megaCorpDollarSettlement.copy(acceptableContracts = nonEmptySetOf(SecureHash.randomSHA256()))).bilateralNetState) // States must not be nettable if the trusted issuers differ val miniCorpIssuer = nonEmptySetOf(Issued(MINI_CORP.ref(1), USD)) @@ -778,9 +779,9 @@ class ObligationTests { @Test fun `extraction of issuance defintion`() { val fiveKDollarsFromMegaToMega = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, - 5000.DOLLARS.quantity, MEGA_CORP_PUBKEY) + 5000.DOLLARS.quantity, MEGA_CORP) val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, - 1000.DOLLARS.quantity, MEGA_CORP_PUBKEY) + 1000.DOLLARS.quantity, MEGA_CORP) // Issuance definitions must match the input assertEquals(fiveKDollarsFromMegaToMega.template, megaCorpDollarSettlement) @@ -791,14 +792,14 @@ class ObligationTests { fun `adding two settlement contracts nets them`() { val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, - 5000.DOLLARS.quantity, MINI_CORP_PUBKEY) + 5000.DOLLARS.quantity, MINI_CORP) val oneKDollarsFromMiniToMega = Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, - 1000.DOLLARS.quantity, MEGA_CORP_PUBKEY) + 1000.DOLLARS.quantity, MEGA_CORP) var actual = fiveKDollarsFromMegaToMini.net(fiveKDollarsFromMegaToMini.copy(quantity = 2000.DOLLARS.quantity)) // Both pay from mega to mini, so we add directly var expected = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, 7000.DOLLARS.quantity, - MINI_CORP_PUBKEY) + MINI_CORP) assertEquals(expected, actual) // Reversing the direction should mean adding the second state subtracts from the first @@ -809,7 +810,7 @@ class ObligationTests { // Trying to add an incompatible state must throw an error assertFailsWith(IllegalArgumentException::class) { fiveKDollarsFromMegaToMini.net(Obligation.State(Lifecycle.NORMAL, MINI_CORP, megaCorpDollarSettlement, 1000.DOLLARS.quantity, - MINI_CORP_PUBKEY)) + MINI_CORP)) } } @@ -817,9 +818,9 @@ class ObligationTests { fun `extracting amounts due between parties from a list of states`() { val megaCorpDollarSettlement = Obligation.Terms(trustedCashContract, megaIssuedDollars, fivePm) val fiveKDollarsFromMegaToMini = Obligation.State(Lifecycle.NORMAL, MEGA_CORP, megaCorpDollarSettlement, - 5000.DOLLARS.quantity, MINI_CORP_PUBKEY) + 5000.DOLLARS.quantity, MINI_CORP) val amount = fiveKDollarsFromMegaToMini.amount - val expected: Map, Amount>> = mapOf(Pair(Pair(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY), Amount(amount.quantity, amount.token.product))) + val expected: Map, Amount>> = mapOf(Pair(Pair(MEGA_CORP, MINI_CORP), Amount(amount.quantity, amount.token.product))) val actual = extractAmountsDue(megaCorpDollarSettlement, listOf(fiveKDollarsFromMegaToMini)) assertEquals(expected, actual) } @@ -827,24 +828,24 @@ class ObligationTests { @Test fun `netting equal balances due between parties`() { // Now try it with two balances, which cancel each other out - val balanced: Map, Amount> = mapOf( - Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)), - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) + val balanced: Map, Amount> = mapOf( + Pair(Pair(ALICE, BOB), Amount(100000000, GBP)), + Pair(Pair(BOB, ALICE), Amount(100000000, GBP)) ) - val expected: Map, Amount> = emptyMap() // Zero balances are stripped before returning - val actual: Map, Amount> = netAmountsDue(balanced) + val expected: Map, Amount> = emptyMap() // Zero balances are stripped before returning + val actual: Map, Amount> = netAmountsDue(balanced) assertEquals(expected, actual) } @Test fun `netting difference balances due between parties`() { // Now try it with two balances, which cancel each other out - val balanced: Map, Amount> = mapOf( - Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)), - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(200000000, GBP)) + val balanced: Map, Amount> = mapOf( + Pair(Pair(ALICE, BOB), Amount(100000000, GBP)), + Pair(Pair(BOB, ALICE), Amount(200000000, GBP)) ) - val expected: Map, Amount> = mapOf( - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) + val expected: Map, Amount> = mapOf( + Pair(Pair(BOB, ALICE), Amount(100000000, GBP)) ) val actual = netAmountsDue(balanced) assertEquals(expected, actual) @@ -852,16 +853,16 @@ class ObligationTests { @Test fun `summing empty balances due between parties`() { - val empty = emptyMap, Amount>() - val expected = emptyMap() + val empty = emptyMap, Amount>() + val expected = emptyMap() val actual = sumAmountsDue(empty) assertEquals(expected, actual) } @Test fun `summing balances due between parties`() { - val simple: Map, Amount> = mapOf(Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP))) - val expected: Map = mapOf(Pair(ALICE_PUBKEY, -100000000L), Pair(BOB_PUBKEY, 100000000L)) + val simple: Map, Amount> = mapOf(Pair(Pair(ALICE, BOB), Amount(100000000, GBP))) + val expected: Map = mapOf(Pair(ALICE, -100000000L), Pair(BOB, 100000000L)) val actual = sumAmountsDue(simple) assertEquals(expected, actual) } @@ -869,11 +870,11 @@ class ObligationTests { @Test fun `summing balances due between parties which net to zero`() { // Now try it with two balances, which cancel each other out - val balanced: Map, Amount> = mapOf( - Pair(Pair(ALICE_PUBKEY, BOB_PUBKEY), Amount(100000000, GBP)), - Pair(Pair(BOB_PUBKEY, ALICE_PUBKEY), Amount(100000000, GBP)) + val balanced: Map, Amount> = mapOf( + Pair(Pair(ALICE, BOB), Amount(100000000, GBP)), + Pair(Pair(BOB, ALICE), Amount(100000000, GBP)) ) - val expected: Map = emptyMap() // Zero balances are stripped before returning + val expected: Map = emptyMap() // Zero balances are stripped before returning val actual = sumAmountsDue(balanced) assertEquals(expected, actual) } diff --git a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt index 1deab780cd..1b9789bc04 100644 --- a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt +++ b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt @@ -10,6 +10,7 @@ import net.corda.core.contracts.CommandData import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionType import net.corda.core.crypto.NullSignature +import net.corda.core.identity.AnonymousParty import net.corda.core.testing.* import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -25,7 +26,7 @@ class ContractStateGenerator : Generator(ContractState::class.jav override fun generate(random: SourceOfRandomness, status: GenerationStatus): ContractState { return Cash.State( amount = AmountGenerator(IssuedGenerator(CurrencyGenerator())).generate(random, status), - owner = PublicKeyGenerator().generate(random, status) + owner = AnonymousParty(PublicKeyGenerator().generate(random, status)) ) } } diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index eb2c99c836..33f15b0898 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -54,8 +54,8 @@ class CashPaymentFlowTests { net.runNetwork() val paymentTx = future.getOrThrow() val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() - val ourState = states.single { it.owner != payTo.owningKey } - val paymentState = states.single { it.owner == payTo.owningKey } + val ourState = states.single { it.owner.owningKey != payTo.owningKey } + val paymentState = states.single { it.owner.owningKey == payTo.owningKey } assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 52657aa6dd..4721ca8aeb 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -4,11 +4,11 @@ import com.google.common.util.concurrent.ListenableFuture import net.corda.contracts.testing.calculateRandomlySizedAmounts import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS -import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.currency import net.corda.core.flows.FlowException import net.corda.core.flows.FlowStateMachine import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.map import net.corda.core.serialization.OpaqueBytes import net.corda.core.transactions.SignedTransaction @@ -40,13 +40,14 @@ class IssuerFlowTest { bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name) // using default IssueTo Party Reference - val issueToPartyAndRef = bankClientNode.info.legalIdentity.ref(OpaqueBytes.Companion.of(123)) - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, issueToPartyAndRef) + val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) // try to issue an amount of a restricted currency assertFailsWith { - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), issueToPartyAndRef).issueRequestResult.getOrThrow() + runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).issueRequestResult.getOrThrow() } bankOfCordaNode.stop() @@ -62,8 +63,8 @@ class IssuerFlowTest { bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name) // using default IssueTo Party Reference - val issueToPartyAndRef = bankOfCordaNode.info.legalIdentity.ref(OpaqueBytes.Companion.of(123)) - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, issueToPartyAndRef) + val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, + bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) bankOfCordaNode.stop() @@ -79,14 +80,12 @@ class IssuerFlowTest { bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name) bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name) - // using default IssueTo Party Reference - val issueToPartyAndRef = bankClientNode.info.legalIdentity.ref(OpaqueBytes.Companion.of(123)) - // this test exercises the Cashflow issue and move subflows to ensure consistent spending of issued states val amount = 10000.DOLLARS val amounts = calculateRandomlySizedAmounts(10000.DOLLARS, 10, 10, Random()) val handles = amounts.map { pennies -> - runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), issueToPartyAndRef) + runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(pennies, amount.token), + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) } handles.forEach { require(it.issueRequestResult.get() is SignedTransaction) @@ -98,13 +97,14 @@ class IssuerFlowTest { } private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode, - amount: Amount, issueToPartyAndRef: PartyAndReference): RunResult { - val resolvedIssuerParty = issuerNode.services.identityService.partyFromAnonymous(issueToPartyAndRef) ?: throw IllegalStateException() + amount: Amount, + party: Party, ref: OpaqueBytes): RunResult { + val issueToPartyAndRef = party.ref(ref) val issuerFuture = issuerNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) { _ -> - IssuerFlow.Issuer(resolvedIssuerParty) + IssuerFlow.Issuer(party) }.map { it.stateMachine } - val issueRequest = IssuanceRequester(amount, resolvedIssuerParty, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) + val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture) diff --git a/gradle-plugins/build.gradle b/gradle-plugins/build.gradle index 4086427a4f..9825dc1106 100644 --- a/gradle-plugins/build.gradle +++ b/gradle-plugins/build.gradle @@ -8,6 +8,8 @@ buildscript { // If you bump this version you must re-bootstrap the codebase. See the README for more information. ext.gradle_plugins_version = constants.getProperty("gradlePluginsVersion") + ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") + ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") repositories { mavenLocal() @@ -37,7 +39,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['cordformation', 'quasar-utils'] + publications = ['cordformation', 'quasar-utils', 'cordform-common'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/gradle-plugins/cordformation/build.gradle b/gradle-plugins/cordformation/build.gradle index a2154f9aa6..72c778d692 100644 --- a/gradle-plugins/cordformation/build.gradle +++ b/gradle-plugins/cordformation/build.gradle @@ -4,8 +4,6 @@ buildscript { file("$projectDir/../../constants.properties").withInputStream { constants.load(it) } ext.kotlin_version = constants.getProperty("kotlinVersion") - ext.bouncycastle_version = constants.getProperty("bouncycastleVersion") - ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") repositories { mavenCentral() @@ -46,11 +44,7 @@ dependencies { noderunner "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - // TypeSafe Config: for simple and human friendly config files. - compile "com.typesafe:config:$typesafe_config_version" - - // Bouncy Castle: for X.500 distinguished name manipulation - compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" + compile project(':cordform-common') } task createNodeRunner(type: Jar, dependsOn: [classes]) { diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy index df353e7a3f..927130f425 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordform.groovy @@ -1,19 +1,28 @@ package net.corda.plugins +import static org.gradle.api.tasks.SourceSet.MAIN_SOURCE_SET_NAME +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformDefinition import org.apache.tools.ant.filters.FixCrLfFilter +import org.bouncycastle.asn1.x500.X500Name import org.gradle.api.DefaultTask +import org.gradle.api.plugins.JavaPluginConvention import org.gradle.api.tasks.TaskAction - import java.nio.file.Path import java.nio.file.Paths + /** * Creates nodes based on the configuration of this task in the gradle configuration DSL. * * See documentation for examples. */ class Cordform extends DefaultTask { - protected Path directory = Paths.get("./build/nodes") - protected List nodes = new ArrayList() + /** + * Optionally the name of a CordformDefinition subclass to which all configuration will be delegated. + */ + String definitionClass + protected def directory = Paths.get("build", "nodes") + private def nodes = new ArrayList() protected String networkMapNodeName /** @@ -42,7 +51,7 @@ class Cordform extends DefaultTask { * @param configureClosure A node configuration that will be deployed. */ void node(Closure configureClosure) { - nodes << project.configure(new Node(project), configureClosure) + nodes << (Node) project.configure(new Node(project), configureClosure) } /** @@ -51,7 +60,7 @@ class Cordform extends DefaultTask { * @param name The name of the node as specified in the node configuration DSL. * @return A node instance. */ - protected Node getNodeByName(String name) { + private Node getNodeByName(String name) { for(Node node : nodes) { if(node.name == name) { return node @@ -64,7 +73,7 @@ class Cordform extends DefaultTask { /** * Installs the run script into the nodes directory. */ - protected void installRunScript() { + private void installRunScript() { project.copy { from Cordformation.getPluginFile(project, "net/corda/plugins/runnodes.jar") fileMode 0755 @@ -85,19 +94,51 @@ class Cordform extends DefaultTask { } } + /** + * The definitionClass needn't be compiled until just before our build method, so we load it manually via sourceSets.main.runtimeClasspath. + */ + private CordformDefinition loadCordformDefinition() { + def plugin = project.convention.getPlugin(JavaPluginConvention.class) + def classpath = plugin.sourceSets.getByName(MAIN_SOURCE_SET_NAME).runtimeClasspath + URL[] urls = classpath.files.collect { it.toURI().toURL() } + (CordformDefinition) new URLClassLoader(urls, CordformDefinition.classLoader).loadClass(definitionClass).newInstance() + } + /** * This task action will create and install the nodes based on the node configurations added. */ @TaskAction void build() { + String networkMapNodeName + if (null != definitionClass) { + def cd = loadCordformDefinition() + networkMapNodeName = cd.networkMapNodeName.toString() + cd.nodeConfigurers.each { nc -> + node { Node it -> + nc.accept it + it.rootDir directory + } + } + cd.setup new CordformContext() { + Path baseDirectory(X500Name nodeName) { + project.projectDir.toPath().resolve(getNodeByName(nodeName.toString()).nodeDir.toPath()) + } + } + } else { + networkMapNodeName = this.networkMapNodeName + nodes.each { + it.rootDir directory + } + } installRunScript() - Node networkMapNode = getNodeByName(networkMapNodeName) + def networkMapNode = getNodeByName(networkMapNodeName) + if (networkMapNode == null) + throw new IllegalStateException("The networkMap property refers to a node that isn't configured ($networkMapNodeName)") nodes.each { if(it != networkMapNode) { it.networkMapAddress(networkMapNode.getP2PAddress(), networkMapNodeName) } - it.build(directory.toFile()) + it.build() } } } - diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy index de65dc45b9..3151343f9a 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Cordformation.groovy @@ -22,7 +22,7 @@ class Cordformation implements Plugin { * @param filePathInJar The file in the JAR, relative to root, you wish to access. * @return A file handle to the file in the JAR. */ - static File getPluginFile(Project project, String filePathInJar) { + protected static File getPluginFile(Project project, String filePathInJar) { return project.rootProject.resources.text.fromArchiveEntry(project.rootProject.buildscript.configurations.classpath.find { it.name.contains('cordformation') }, filePathInJar).asFile() diff --git a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy index ea92802237..3f3a53c8f2 100644 --- a/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy +++ b/gradle-plugins/cordformation/src/main/groovy/net/corda/plugins/Node.groovy @@ -1,33 +1,21 @@ package net.corda.plugins import com.typesafe.config.* +import net.corda.cordform.CordformNode import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.style.BCStyle import org.gradle.api.Project import java.nio.charset.StandardCharsets import java.nio.file.Files +import java.nio.file.Path /** * Represents a node that will be installed. */ -class Node { +class Node extends CordformNode { static final String NODEJAR_NAME = 'corda.jar' static final String WEBJAR_NAME = 'corda-webserver.jar' - static final String DEFAULT_HOST = 'localhost' - /** - * Name of the node. - */ - public String name - /** - * A list of advertised services ID strings. - */ - protected List advertisedServices = [] - - /** - * If running a distributed notary, a list of node addresses for joining the Raft cluster - */ - protected List notaryClusterAddresses = [] /** * Set the list of CorDapps to install to the plugins directory. Each cordapp is a fully qualified Maven * dependency name, eg: com.example:product-name:0.1 @@ -35,39 +23,10 @@ class Node { * @note Your app will be installed by default and does not need to be included here. */ protected List cordapps = [] - /** - * Set the RPC users for this node. This configuration block allows arbitrary configuration. - * The recommended current structure is: - * [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]] - * The above is a list to a map of keys to values using Groovy map and list shorthands. - * - * @note Incorrect configurations will not cause a DSL error. - */ - protected List> rpcUsers = [] - private Config config = ConfigFactory.empty() - private File nodeDir + protected File nodeDir private Project project - /** - * Set the name of the node. - * - * @param name The node name. - */ - void name(String name) { - this.name = name - config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name)) - } - - /** - * Set the nearest city to the node. - * - * @param nearestCity The name of the nearest city to the node. - */ - void nearestCity(String nearestCity) { - config = config.withValue("nearestCity", ConfigValueFactory.fromAnyRef(nearestCity)) - } - /** * Sets whether this node will use HTTPS communication. * @@ -88,26 +47,6 @@ class Node { config = config.withValue("useTestClock", ConfigValueFactory.fromAnyRef(useTestClock)) } - /** - * Set the Artemis P2P port for this node. - * - * @param p2pPort The Artemis messaging queue port. - */ - void p2pPort(Integer p2pPort) { - config = config.withValue("p2pAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$p2pPort".toString())) - } - - /** - * Set the Artemis RPC port for this node. - * - * @param rpcPort The Artemis RPC queue port. - */ - void rpcPort(Integer rpcPort) { - config = config.withValue("rpcAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$rpcPort".toString())) - } - /** * Set the HTTP web server port for this node. * @@ -118,14 +57,6 @@ class Node { ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$webPort".toString())) } - /** - * Set the port which to bind the Copycat (Raft) node to - */ - void notaryNodePort(Integer notaryPort) { - config = config.withValue("notaryNodeAddress", - ConfigValueFactory.fromAnyRef("$DEFAULT_HOST:$notaryPort".toString())) - } - /** * Set the network map address for this node. * @@ -155,16 +86,19 @@ class Node { this.project = project } - void build(File rootDir) { + protected void rootDir(Path rootDir) { def dirName try { X500Name x500Name = new X500Name(name) dirName = x500Name.getRDNs(BCStyle.CN).getAt(0).getFirst().getValue().toString() - } catch(IllegalArgumentException ex) { + } catch(IllegalArgumentException ignore) { // Can't parse as an X500 name, use the full string dirName = name } - nodeDir = new File(rootDir, dirName.replaceAll("\\s","")) + nodeDir = new File(rootDir.toFile(), dirName.replaceAll("\\s","")) + } + + protected void build() { configureRpcUsers() installCordaJar() installWebserverJar() diff --git a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt index 907a9126a4..0107b26e8b 100644 --- a/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt +++ b/gradle-plugins/cordformation/src/noderunner/kotlin/net/corda/plugins/NodeRunner.kt @@ -87,14 +87,15 @@ private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: I activate tell app "System Events" to tell process "Terminal" to keystroke "t" using command down delay 0.5 - do script "bash -c 'cd $dir; /usr/libexec/java_home -v 1.8 --exec ${command.joinToString(" ")} && exit'" in selected tab of the front window + do script "bash -c 'cd $dir; ${command.joinToString(" ")} && exit'" in selected tab of the front window end tell""") } OS.WINDOWS -> { listOf("cmd", "/C", "start ${command.joinToString(" ")}") } OS.LINUX -> { - val command = "${unixCommand()} || sh" + // Start shell to keep window open unless java terminated normally or due to SIGTERM: + val command = "${unixCommand()}; [ $? -eq 0 -o $? -eq 143 ] || sh" if (isTmux()) { listOf("tmux", "new-window", "-n", nodeName, command) } else { diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index 50e3950b93..061db29b3a 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -1,4 +1,6 @@ rootProject.name = 'corda-gradle-plugins' include 'publish-utils' include 'quasar-utils' -include 'cordformation' \ No newline at end of file +include 'cordformation' +include 'cordform-common' +project(':cordform-common').projectDir = new File("$settingsDir/../cordform-common") diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt index 0f16c2e056..b56f52239a 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/ArtemisMessagingComponent.kt @@ -30,6 +30,7 @@ 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 IP_REQUEST_PREFIX = "ip." const val P2P_QUEUE = "p2p.inbound" const val NOTIFICATIONS_ADDRESS = "${INTERNAL_PREFIX}activemq.notifications" const val NETWORK_MAP_QUEUE = "${INTERNAL_PREFIX}networkmap" diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt index d7d70a5428..bda75a2bd4 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -18,6 +18,11 @@ data class User( val password: String, val permissions: Set) { override fun toString(): String = "${javaClass.simpleName}($username, permissions=$permissions)" + fun toMap() = mapOf( + "username" to username, + "password" to password, + "permissions" to permissions + ) } /** Records the protocol version in which this RPC was added. */ diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt index 0df1ea3e89..31e9743eee 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/serialization/DefaultWhitelist.kt @@ -18,6 +18,7 @@ import java.util.* class DefaultWhitelist : CordaPluginRegistry() { override fun customizeSerialization(custom: SerializationCustomization): Boolean { custom.apply { + // TODO: Turn this into an array and use map {} addToWhitelist(Array(0, {}).javaClass) addToWhitelist(Notification::class.java) addToWhitelist(Notification.Kind::class.java) @@ -41,9 +42,23 @@ class DefaultWhitelist : CordaPluginRegistry() { addToWhitelist(java.lang.Class::class.java) addToWhitelist(java.math.BigDecimal::class.java) addToWhitelist(java.security.KeyPair::class.java) + + // Matches the list in TimeSerializers.addDefaultSerializers: addToWhitelist(java.time.Duration::class.java) addToWhitelist(java.time.Instant::class.java) addToWhitelist(java.time.LocalDate::class.java) + addToWhitelist(java.time.LocalDateTime::class.java) + addToWhitelist(java.time.ZoneOffset::class.java) + addToWhitelist(java.time.ZoneId::class.java) + addToWhitelist(java.time.OffsetTime::class.java) + addToWhitelist(java.time.OffsetDateTime::class.java) + addToWhitelist(java.time.ZonedDateTime::class.java) + addToWhitelist(java.time.Year::class.java) + addToWhitelist(java.time.YearMonth::class.java) + addToWhitelist(java.time.MonthDay::class.java) + addToWhitelist(java.time.Period::class.java) + addToWhitelist(java.time.DayOfWeek::class.java) // No custom serialiser but it's an enum. + addToWhitelist(java.util.Collections.singletonMap("A", "B").javaClass) addToWhitelist(java.util.LinkedHashMap::class.java) addToWhitelist(BigDecimal::class.java) diff --git a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt index addad4367a..3a09140194 100644 --- a/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt +++ b/node-api/src/test/kotlin/net/corda/nodeapi/config/ConfigParsingTest.kt @@ -12,12 +12,10 @@ import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import java.net.URL import java.nio.file.Path -import java.nio.file.Paths import java.time.Instant import java.time.LocalDate import java.util.* import kotlin.reflect.full.primaryConstructor -import kotlin.test.assertEquals class ConfigParsingTest { @Test @@ -70,7 +68,7 @@ class ConfigParsingTest { @Test fun `Path`() { - val path = Paths.get("tmp") / "test" + val path = "tmp" / "test" testPropertyType(path, path / "file", valuesToString = true) } diff --git a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt index f2899c2102..05c5c3f903 100644 --- a/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt +++ b/node-schemas/src/test/kotlin/net/corda/node/services/vault/schemas/VaultSchemaTest.kt @@ -11,7 +11,11 @@ import io.requery.rx.KotlinRxEntityStore import io.requery.sql.* import io.requery.sql.platform.Generic import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.schemas.requery.converters.InstantConverter @@ -19,17 +23,16 @@ import net.corda.core.schemas.requery.converters.VaultStateStatusConverter import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.LedgerTransaction +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.DUMMY_PUBKEY_1 -import net.corda.core.utilities.DUMMY_PUBKEY_2 import org.h2.jdbcx.JdbcDataSource import org.junit.After import org.junit.Assert import org.junit.Before import org.junit.Test import rx.Observable -import java.security.PublicKey import java.time.Instant import java.util.* import java.util.concurrent.CountDownLatch @@ -81,12 +84,12 @@ class VaultSchemaTest { private class VaultNoopContract : Contract { override val legalContractReference = SecureHash.sha256("") - data class VaultNoopState(override val owner: PublicKey) : OwnableState { + data class VaultNoopState(override val owner: AbstractParty) : OwnableState { override val contract = VaultNoopContract() - override val participants: List + override val participants: List get() = listOf(owner) - override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Create(), copy(owner = newOwner)) + override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Create(), copy(owner = newOwner)) } interface Commands : CommandData { @@ -101,10 +104,10 @@ class VaultSchemaTest { private fun setupDummyData() { // dummy Transaction val notary: Party = DUMMY_NOTARY - val inState1 = TransactionState(DummyContract.SingleOwnerState(0, DUMMY_PUBKEY_1), notary) + val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) val inState2 = TransactionState(DummyContract.MultiOwnerState(0, - listOf(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2)), notary) - val inState3 = TransactionState(VaultNoopContract.VaultNoopState(DUMMY_PUBKEY_1), notary) + listOf(ALICE, BOB)), notary) + val inState3 = TransactionState(VaultNoopContract.VaultNoopState(ALICE), notary) val outState1 = inState1.copy() val outState2 = inState2.copy() val outState3 = inState3.copy() @@ -132,9 +135,9 @@ class VaultSchemaTest { private fun createTxnWithTwoStateTypes(): LedgerTransaction { val notary: Party = DUMMY_NOTARY - val inState1 = TransactionState(DummyContract.SingleOwnerState(0, DUMMY_PUBKEY_1), notary) + val inState1 = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary) val inState2 = TransactionState(DummyContract.MultiOwnerState(0, - listOf(DUMMY_PUBKEY_1, DUMMY_PUBKEY_2)), notary) + listOf(ALICE, BOB)), notary) val outState1 = inState1.copy() val outState2 = inState2.copy() val state1TxHash = SecureHash.randomSHA256() diff --git a/node/build.gradle b/node/build.gradle index 7520a85a70..4dc0c02b44 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -54,6 +54,7 @@ dependencies { compile project(':node-schemas') compile project(':node-api') compile project(':client:rpc') + compile project(':cordform-common') compile "com.google.code.findbugs:jsr305:3.0.1" @@ -125,7 +126,8 @@ dependencies { compile "org.hibernate:hibernate-java8:$hibernate_version" // Capsule is a library for building independently executable fat JARs. - compile 'co.paralleluniverse:capsule:1.0.3' + // We only need this dependency to compile our Caplet against. + compileOnly "co.paralleluniverse:capsule:$capsule_version" // Java Atomix: RAFT library compile 'io.atomix.copycat:copycat-client:1.2.3' @@ -150,6 +152,9 @@ dependencies { // Requery: object mapper for Kotlin compile "io.requery:requery-kotlin:$requery_version" + // FastClasspathScanner: classpath scanning + compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20' + // Integration test helpers integrationTestCompile "junit:junit:$junit_version" } diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index a3d4c6e650..92250649a3 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -19,24 +19,25 @@ configurations { sourceCompatibility = 1.6 targetCompatibility = 1.6 -dependencies { - compile project(':node') -} - -task buildCordaJAR(type: FatCapsule) { +task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationClass 'net.corda.node.Corda' archiveName "corda-${corda_release_version}.jar" applicationSource = files( - project.tasks.findByName('jar'), + project(':node').configurations.compile, + project(':node').jar, '../build/classes/main/CordaCaplet.class', '../build/classes/main/CordaCaplet$1.class', "$rootDir/config/dev/log4j2.xml" ) from 'NOTICE' // Copy CDDL notice + capsuleManifest { applicationVersion = corda_release_version appClassPath = ["jolokia-agent-war-${project.rootProject.ext.jolokia_version}.war"] + // TODO add this once we upgrade quasar to 0.7.8 + // def quasarExcludeExpression = "x(rx**;io**;kotlin**;jdk**;reflectasm**;groovyjarjarasm**;groovy**;joptsimple**;groovyjarjarantlr**;javassist**;com.fasterxml**;com.typesafe**;com.google**;com.zaxxer**;com.jcabi**;com.codahale**;com.esotericsoftware**;de.javakaffee**;org.objectweb**;org.slf4j**;org.w3c**;org.codehaus**;org.h2**;org.crsh**;org.fusesource**;org.hibernate**;org.dom4j**;org.bouncycastle**;org.apache**;org.objenesis**;org.jboss**;org.xml**;org.jcp**;org.jetbrains**;org.yaml**;co.paralleluniverse**;net.i2p**)" + // javaAgents = ["quasar-core-${quasar_version}-jdk8.jar=${quasarExcludeExpression}"] javaAgents = ["quasar-core-${quasar_version}-jdk8.jar"] systemProperties['visualvm.display.name'] = 'Corda' systemProperties['jdk.serialFilter'] = 'maxbytes=0' diff --git a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt index 6f67459650..0884300518 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -1,10 +1,11 @@ package net.corda.node +import co.paralleluniverse.fibers.Suspendable import net.corda.core.div import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow -import net.corda.core.node.CordaPluginRegistry import net.corda.core.utilities.ALICE import net.corda.node.driver.driver import net.corda.node.services.startFlowPermission @@ -48,18 +49,12 @@ class BootTests { } } +@StartableByRPC class ObjectInputStreamFlow : FlowLogic() { - + @Suspendable override fun call() { System.clearProperty("jdk.serialFilter") // This checks that the node has already consumed the property. val data = ByteArrayOutputStream().apply { ObjectOutputStream(this).use { it.writeObject(object : Serializable {}) } }.toByteArray() ObjectInputStream(data.inputStream()).use { it.readObject() } } - -} - -class BootTestsPlugin : CordaPluginRegistry() { - - override val requiredFlows: Map> = mapOf(ObjectInputStreamFlow::class.java.name to setOf()) - } diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt new file mode 100644 index 0000000000..0c4d48600b --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -0,0 +1,29 @@ +package net.corda.node + +import com.google.common.base.Stopwatch +import net.corda.node.driver.NetworkMapStartStrategy +import net.corda.node.driver.driver +import org.junit.Ignore +import org.junit.Test +import java.util.* +import java.util.concurrent.TimeUnit + +@Ignore("Only use locally") +class NodeStartupPerformanceTests { + + // Measure the startup time of nodes. Note that this includes an RPC roundtrip, which causes e.g. Kryo initialisation. + @Test + fun `single node startup time`() { + driver(networkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false)) { + startDedicatedNetworkMapService().get() + val times = ArrayList() + for (i in 1 .. 10) { + val time = Stopwatch.createStarted().apply { + startNode().get() + }.stop().elapsed(TimeUnit.MICROSECONDS) + times.add(time) + } + println(times.map { it / 1_000_000.0 }) + } + } +} diff --git a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt index 6857a64543..c85ce319b1 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/BFTNotaryServiceTests.kt @@ -23,7 +23,6 @@ import net.corda.node.utilities.transaction import net.corda.testing.node.NodeBasedTest import org.bouncycastle.asn1.x500.X500Name import org.junit.Test -import java.security.KeyPair import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -43,25 +42,21 @@ class BFTNotaryServiceTests : NodeBasedTest() { val alice = startNode(ALICE.name).getOrThrow() val notaryParty = alice.netMapCache.getNotary(notaryCommonName)!! - val notaryNodeKeyPair = with(masterNode) { database.transaction { services.notaryIdentityKey } } - val aliceKey = with(alice) { database.transaction { services.legalIdentityKey } } - val inputState = issueState(alice, notaryParty, notaryNodeKeyPair) + val inputState = issueState(alice, notaryParty) - val firstSpendTx = TransactionType.General.Builder(notaryParty).withItems(inputState).run { - signWith(aliceKey) - toSignedTransaction(false) - } + val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) + val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) 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) + val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).run { + val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity) addOutputState(dummyState) - signWith(aliceKey) - toSignedTransaction(false) + this } + val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } @@ -69,14 +64,12 @@ class BFTNotaryServiceTests : NodeBasedTest() { assertEquals(error.txId, secondSpendTx.id) } - private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> { + private fun issueState(node: AbstractNode, notary: Party): StateAndRef<*> { return node.database.transaction { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) - tx.signWith(node.services.legalIdentityKey) - tx.signWith(notaryKey) - val stx = tx.toSignedTransaction() + val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) + val stx = node.services.signInitialTransaction(builder) node.services.recordTransactions(listOf(stx)) - StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) + StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt index 3e5edba828..1cba3b0b44 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/RaftNotaryServiceTests.kt @@ -17,7 +17,6 @@ import net.corda.node.utilities.transaction import net.corda.testing.node.NodeBasedTest import org.bouncycastle.asn1.x500.X500Name import org.junit.Test -import java.security.KeyPair import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith @@ -33,24 +32,21 @@ class RaftNotaryServiceTests : NodeBasedTest() { ).getOrThrow() val notaryParty = alice.netMapCache.getNotary(notaryName)!! - val notaryNodeKeyPair = with(masterNode) { database.transaction { services.notaryIdentityKey } } - val aliceKey = with(alice) { database.transaction { services.legalIdentityKey } } - val inputState = issueState(alice, notaryParty, notaryNodeKeyPair) + val inputState = issueState(alice, notaryParty) + + val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) + val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) - 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) + val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).run { + val dummyState = DummyContract.SingleOwnerState(0, alice.info.legalIdentity) addOutputState(dummyState) - signWith(aliceKey) - toSignedTransaction(false) + this } + val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) val ex = assertFailsWith(NotaryException::class) { secondSpend.resultFuture.getOrThrow() } @@ -58,14 +54,12 @@ class RaftNotaryServiceTests : NodeBasedTest() { assertEquals(error.txId, secondSpendTx.id) } - private fun issueState(node: AbstractNode, notary: Party, notaryKey: KeyPair): StateAndRef<*> { + private fun issueState(node: AbstractNode, notary: Party): StateAndRef<*> { return node.database.transaction { - val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) - tx.signWith(node.services.legalIdentityKey) - tx.signWith(notaryKey) - val stx = tx.toSignedTransaction() + val builder = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) + val stx = node.services.signInitialTransaction(builder) node.services.recordTransactions(listOf(stx)) - StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) + StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) } } } \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt index 649fbb5054..cbe6cb973d 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PSecurityTest.kt @@ -70,7 +70,7 @@ class P2PSecurityTest : NodeBasedTest() { private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public), MOCK_VERSION_INFO.platformVersion) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) - val request = RegistrationRequest(registration.toWire(identity.private), net.myAddress) + val request = RegistrationRequest(registration.toWire(keyService, identity.public), net.myAddress) return net.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress) } } diff --git a/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 4734e517b7..0000000000 --- a/node/src/integration-test/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1 +0,0 @@ -net.corda.node.BootTestsPlugin diff --git a/node/src/main/kotlin/net/corda/node/ArgsParser.kt b/node/src/main/kotlin/net/corda/node/ArgsParser.kt index 6292608493..6168e4b99b 100644 --- a/node/src/main/kotlin/net/corda/node/ArgsParser.kt +++ b/node/src/main/kotlin/net/corda/node/ArgsParser.kt @@ -65,9 +65,7 @@ data class CmdLineOptions(val baseDirectory: Path, val isVersion: Boolean, val noLocalShell: Boolean, val sshdServer: Boolean) { - fun loadConfig(allowMissingConfig: Boolean = false, configOverrides: Map = emptyMap()): FullNodeConfiguration { - return ConfigHelper - .loadConfig(baseDirectory, configFile, allowMissingConfig, configOverrides) + fun loadConfig() = ConfigHelper + .loadConfig(baseDirectory, configFile) .parseAs() - } } diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 462ab777f0..88920dea15 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -6,6 +6,8 @@ import com.jcabi.manifests.Manifests import com.typesafe.config.ConfigException import joptsimple.OptionException import net.corda.core.* +import net.corda.core.crypto.commonName +import net.corda.core.crypto.orgName import net.corda.core.node.VersionInfo import net.corda.core.utilities.Emoji import net.corda.core.utilities.LogHelper.withLevel @@ -146,7 +148,9 @@ fun main(args: Array) { node.networkMapRegistrationFuture.success { val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 - printBasicNodeInfo("Node for \"${node.info.legalIdentity.name}\" started up and registered in $elapsed sec") + // TODO: Replace this with a standard function to get an unambiguous rendering of the X.500 name. + val name = node.info.legalIdentity.name.orgName ?: node.info.legalIdentity.name.commonName + printBasicNodeInfo("Node for \"$name\" started up and registered in $elapsed sec") // Don't start the shell if there's no console attached. val runShell = !cmdlineOptions.noLocalShell && System.console() != null diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/node/src/main/kotlin/net/corda/node/driver/Driver.kt index 4e4a8047ee..80b51deed6 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/node/src/main/kotlin/net/corda/node/driver/Driver.kt @@ -18,9 +18,11 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.* import net.corda.node.LOGS_DIRECTORY_NAME +import net.corda.node.services.config.* import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.VerifierType +import net.corda.node.services.config.configOf import net.corda.node.services.network.NetworkMapService import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.ServiceIdentityGenerator @@ -28,6 +30,8 @@ import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.nodeapi.User import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.parseAs +import net.corda.cordform.CordformNode +import net.corda.cordform.CordformContext import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name @@ -59,7 +63,7 @@ private val log: Logger = loggerFor() /** * This is the interface that's exposed to DSL users. */ -interface DriverDSLExposedInterface { +interface DriverDSLExposedInterface : CordformContext { /** * Starts a [net.corda.node.internal.Node] in a separate process. * @@ -76,6 +80,8 @@ interface DriverDSLExposedInterface { verifierType: VerifierType = VerifierType.InMemory, customOverrides: Map = emptyMap()): ListenableFuture + fun startNodes(nodes: List): List> + /** * Starts a distributed notary cluster. * @@ -102,9 +108,9 @@ interface DriverDSLExposedInterface { /** * Starts a network map service node. Note that only a single one should ever be running, so you will probably want - * to set automaticallyStartNetworkMap to false in your [driver] call. + * to set networkMapStartStrategy to Dedicated(false) in your [driver] call. */ - fun startNetworkMapService() + fun startDedicatedNetworkMapService(): ListenableFuture fun waitForAllNodesToFinish() @@ -201,7 +207,7 @@ fun driver( debugPortAllocation: PortAllocation = PortAllocation.Incremental(5005), systemProperties: Map = emptyMap(), useTestClock: Boolean = false, - automaticallyStartNetworkMap: Boolean = true, + networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), dsl: DriverDSLExposedInterface.() -> A ) = genericDriver( driverDsl = DriverDSL( @@ -210,7 +216,7 @@ fun driver( systemProperties = systemProperties, driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, - automaticallyStartNetworkMap = automaticallyStartNetworkMap, + networkMapStartStrategy = networkMapStartStrategy, isDebug = isDebug ), coerce = { it }, @@ -381,6 +387,28 @@ class ShutdownManager(private val executorService: ExecutorService) { } registerShutdown(processShutdown) } + + interface Follower { + fun unfollow() + fun shutdown() + } + + fun follower() = object : Follower { + private val start = state.locked { registeredShutdowns.size } + private val end = AtomicInteger(start - 1) + override fun unfollow() = end.set(state.locked { registeredShutdowns.size }) + override fun shutdown() = end.get().let { end -> + start > end && throw IllegalStateException("You haven't called unfollow.") + state.locked { + registeredShutdowns.subList(start, end).listIterator(end - start).run { + while (hasPrevious()) { + previous().getOrThrow().invoke() + set(Futures.immediateFuture {}) // Don't break other followers by doing a remove. + } + } + } + } + } } class DriverDSL( @@ -390,14 +418,13 @@ class DriverDSL( val driverDirectory: Path, val useTestClock: Boolean, val isDebug: Boolean, - val automaticallyStartNetworkMap: Boolean + val networkMapStartStrategy: NetworkMapStartStrategy ) : DriverDSLInternalInterface { - private val networkMapLegalName = DUMMY_MAP.name - private val networkMapAddress = portAllocation.nextHostAndPort() - val executorService: ListeningScheduledExecutorService = MoreExecutors.listeningDecorator( - Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) - ) - override val shutdownManager = ShutdownManager(executorService) + private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort() + var _executorService: ListeningScheduledExecutorService? = null + val executorService get() = _executorService!! + var _shutdownManager: ShutdownManager? = null + override val shutdownManager get() = _shutdownManager!! class State { val processes = ArrayList>() @@ -428,8 +455,8 @@ class DriverDSL( } override fun shutdown() { - shutdownManager.shutdown() - executorService.shutdown() + _shutdownManager?.shutdown() + _executorService?.shutdownNow() } private fun establishRpc(nodeAddress: HostAndPort, sslConfig: SSLConfiguration): ListenableFuture { @@ -446,50 +473,59 @@ class DriverDSL( } } + private fun networkMapServiceConfigLookup(networkMapCandidates: List): (X500Name) -> Map? { + return networkMapStartStrategy.run { + when (this) { + is NetworkMapStartStrategy.Dedicated -> { + serviceConfig(dedicatedNetworkMapAddress).let { + { _: X500Name -> it } + } + } + is NetworkMapStartStrategy.Nominated -> { + serviceConfig(HostAndPort.fromString(networkMapCandidates.filter { + it.name == legalName.toString() + }.single().config.getString("p2pAddress"))).let { + { nodeName: X500Name -> if (nodeName == legalName) null else it } + } + } + } + } + } + override fun startNode( providedName: X500Name?, advertisedServices: Set, rpcUsers: List, verifierType: VerifierType, - customOverrides: Map - ): ListenableFuture { + customOverrides: Map): ListenableFuture { val p2pAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() - val debugPort = if (isDebug) debugPortAllocation.nextPort() else null // TODO: Derive name from the full picked name, don't just wrap the common name - val name = providedName ?: X509Utilities.getDevX509Name("${pickA(name).commonName}-${p2pAddress.port}") - val baseDirectory = driverDirectory / name.commonName - val configOverrides = mapOf( + val name = providedName ?: X509Utilities.getDevX509Name("${oneOf(names).commonName}-${p2pAddress.port}") + return startNode(p2pAddress, webAddress, name, configOf( "myLegalName" to name.toString(), "p2pAddress" to p2pAddress.toString(), "rpcAddress" to rpcAddress.toString(), "webAddress" to webAddress.toString(), "extraAdvertisedServiceIds" to advertisedServices.map { it.toString() }, - "networkMapService" to mapOf( - "address" to networkMapAddress.toString(), - "legalName" to networkMapLegalName.toString() - ), + "networkMapService" to networkMapServiceConfigLookup(emptyList())(name), "useTestClock" to useTestClock, - "rpcUsers" to rpcUsers.map { - mapOf( - "username" to it.username, - "password" to it.password, - "permissions" to it.permissions - ) - }, + "rpcUsers" to rpcUsers.map { it.toMap() }, "verifierType" to verifierType.name - ) + customOverrides + ) + customOverrides) + } + private fun startNode(p2pAddress: HostAndPort, webAddress: HostAndPort, nodeName: X500Name, configOverrides: Config) = run { + val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory, + baseDirectory = baseDirectory(nodeName), allowMissingConfig = true, configOverrides = configOverrides) val configuration = config.parseAs() - val processFuture = startNode(executorService, configuration, config, quasarJarPath, debugPort, systemProperties) registerProcess(processFuture) - return processFuture.flatMap { process -> + processFuture.flatMap { process -> // We continue to use SSL enabled port for RPC when its for node user. establishRpc(p2pAddress, configuration).flatMap { rpc -> rpc.waitUntilRegisteredWithNetworkMap().map { @@ -499,6 +535,22 @@ class DriverDSL( } } + override fun startNodes(nodes: List): List> { + val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) + return nodes.map { + val p2pAddress = HostAndPort.fromString(it.config.getString("p2pAddress")); portAllocation.nextHostAndPort() + portAllocation.nextHostAndPort() // rpcAddress + val webAddress = portAllocation.nextHostAndPort() + val name = X500Name(it.name) + startNode(p2pAddress, webAddress, name, it.config + mapOf( + "extraAdvertisedServiceIds" to it.advertisedServices, + "networkMapService" to networkMapServiceConfigLookup(name), + "rpcUsers" to it.rpcUsers, + "notaryClusterAddresses" to it.notaryClusterAddresses + )) + } + } + override fun startNotaryCluster( notaryName: X500Name, clusterSize: Int, @@ -507,7 +559,7 @@ class DriverDSL( rpcUsers: List ): ListenableFuture>> { val nodeNames = (1..clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(it.toString()) } - val paths = nodeNames.map { driverDirectory / it.commonName } + val paths = nodeNames.map { baseDirectory(it) } ServiceIdentityGenerator.generateToDisk(paths, type.id, notaryName) val serviceInfo = ServiceInfo(type, notaryName) @@ -556,24 +608,30 @@ class DriverDSL( } override fun start() { - if (automaticallyStartNetworkMap) { - startNetworkMapService() + _executorService = MoreExecutors.listeningDecorator( + Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) + ) + _shutdownManager = ShutdownManager(executorService) + if (networkMapStartStrategy.startDedicated) { + startDedicatedNetworkMapService() } } - override fun startNetworkMapService() { + override fun baseDirectory(nodeName: X500Name) = driverDirectory / nodeName.commonName.replace(WHITESPACE, "") + + override fun startDedicatedNetworkMapService(): ListenableFuture { val debugPort = if (isDebug) debugPortAllocation.nextPort() else null val apiAddress = portAllocation.nextHostAndPort().toString() - val baseDirectory = driverDirectory / networkMapLegalName.commonName + val networkMapLegalName = networkMapStartStrategy.legalName val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory, + baseDirectory = baseDirectory(networkMapLegalName), allowMissingConfig = true, - configOverrides = mapOf( + configOverrides = configOf( "myLegalName" to networkMapLegalName.toString(), // TODO: remove the webAddress as NMS doesn't need to run a web server. This will cause all // node port numbers to be shifted, so all demos and docs need to be updated accordingly. "webAddress" to apiAddress, - "p2pAddress" to networkMapAddress.toString(), + "p2pAddress" to dedicatedNetworkMapAddress.toString(), "useTestClock" to useTestClock ) ) @@ -581,6 +639,7 @@ class DriverDSL( log.info("Starting network-map-service") val startNode = startNode(executorService, config.parseAs(), config, quasarJarPath, debugPort, systemProperties) registerProcess(startNode) + return startNode.flatMap { addressMustBeBound(executorService, dedicatedNetworkMapAddress, it) } } override fun pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): ListenableFuture { @@ -590,13 +649,13 @@ class DriverDSL( } companion object { - val name = arrayOf( + private val names = arrayOf( ALICE.name, BOB.name, DUMMY_BANK_A.name ) - fun pickA(array: Array): A = array[Math.abs(Random().nextInt()) % array.size] + private fun oneOf(array: Array) = array[Random().nextInt(array.size)] private fun startNode( executorService: ListeningScheduledExecutorService, @@ -615,6 +674,10 @@ class DriverDSL( "visualvm.display.name" to "corda-${nodeConf.myLegalName}", "java.io.tmpdir" to System.getProperty("java.io.tmpdir") // Inherit from parent process ) + // TODO Add this once we upgrade to quasar 0.7.8, this causes startup time to halve. + // val excludePattern = x(rx**;io**;kotlin**;jdk**;reflectasm**;groovyjarjarasm**;groovy**;joptsimple**;groovyjarjarantlr**;javassist**;com.fasterxml**;com.typesafe**;com.google**;com.zaxxer**;com.jcabi**;com.codahale**;com.esotericsoftware**;de.javakaffee**;org.objectweb**;org.slf4j**;org.w3c**;org.codehaus**;org.h2**;org.crsh**;org.fusesource**;org.hibernate**;org.dom4j**;org.bouncycastle**;org.apache**;org.objenesis**;org.jboss**;org.xml**;org.jcp**;org.jetbrains**;org.yaml**;co.paralleluniverse**;net.i2p**)" + // val extraJvmArguments = systemProperties.map { "-D${it.key}=${it.value}" } + + // "-javaagent:$quasarJarPath=$excludePattern" val extraJvmArguments = systemProperties.map { "-D${it.key}=${it.value}" } + "-javaagent:$quasarJarPath" val loggingLevel = if (debugPort == null) "INFO" else "DEBUG" @@ -660,3 +723,4 @@ fun writeConfig(path: Path, filename: String, config: Config) { path.toFile().mkdirs() File("$path/$filename").writeText(config.root().render(ConfigRenderOptions.defaults())) } + diff --git a/node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt b/node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt new file mode 100644 index 0000000000..218dfe5716 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt @@ -0,0 +1,23 @@ +package net.corda.node.driver + +import com.google.common.net.HostAndPort +import net.corda.core.utilities.DUMMY_MAP +import org.bouncycastle.asn1.x500.X500Name + +sealed class NetworkMapStartStrategy { + internal abstract val startDedicated: Boolean + internal abstract val legalName: X500Name + internal fun serviceConfig(address: HostAndPort) = mapOf( + "address" to address.toString(), + "legalName" to legalName.toString() + ) + + class Dedicated(startAutomatically: Boolean) : NetworkMapStartStrategy() { + override val startDedicated = startAutomatically + override val legalName = DUMMY_MAP.name + } + + class Nominated(override val legalName: X500Name) : NetworkMapStartStrategy() { + override val startDedicated = false + } +} diff --git a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt index b6a12c91b4..dc99af9e4c 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -5,15 +5,13 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.MoreExecutors import com.google.common.util.concurrent.SettableFuture +import io.github.lukehutch.fastclasspathscanner.FastClasspathScanner import net.corda.core.* -import net.corda.core.contracts.Amount -import net.corda.core.contracts.PartyAndReference -import net.corda.core.crypto.KeyStoreUtilities -import net.corda.core.crypto.X509Utilities -import net.corda.core.crypto.replaceCommonName +import net.corda.core.crypto.* import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC import net.corda.core.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps @@ -21,10 +19,8 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.* import net.corda.core.node.services.* import net.corda.core.node.services.NetworkMapCache.MapChange -import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize -import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug import net.corda.flows.* @@ -47,7 +43,6 @@ import net.corda.node.services.network.PersistentNetworkMapService import net.corda.node.services.persistence.* import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.statemachine.flowVersion @@ -61,11 +56,15 @@ import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction import org.apache.activemq.artemis.utils.ReusableLatch import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.jetbrains.exposed.sql.Database import org.slf4j.Logger import java.io.IOException +import java.lang.reflect.Modifier.* +import java.net.URL import java.nio.file.FileAlreadyExistsException import java.nio.file.Path +import java.nio.file.Paths import java.security.KeyPair import java.security.KeyStoreException import java.time.Clock @@ -73,6 +72,7 @@ import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS +import kotlin.collections.ArrayList import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -90,18 +90,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val advertisedServices: Set, 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" - - val defaultFlowWhiteList: Map>, Set>> = mapOf( - CashExitFlow::class.java to setOf(Amount::class.java, PartyAndReference::class.java), - CashIssueFlow::class.java to setOf(Amount::class.java, OpaqueBytes::class.java, Party::class.java), - CashPaymentFlow::class.java to setOf(Amount::class.java, Party::class.java), - FinalityFlow::class.java to setOf(LinkedHashSet::class.java), - ContractUpgradeFlow::class.java to emptySet() - ) - } // TODO: Persist this, as well as whether the node is registered. /** @@ -133,10 +121,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, override val schemaService: SchemaService get() = schemas override val transactionVerifierService: TransactionVerifierService get() = txVerifierService override val auditService: AuditService get() = auditService + override val rpcFlows: List>> get() = this@AbstractNode.rpcFlows // Internal only override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - override val flowLogicRefFactory: FlowLogicRefFactoryInternal get() = flowLogicFactory override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { return serverThread.fetchFrom { smm.add(logic, flowInitiator) } @@ -176,13 +164,13 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, lateinit var net: MessagingService lateinit var netMapCache: NetworkMapCacheInternal lateinit var scheduler: NodeSchedulerService - lateinit var flowLogicFactory: FlowLogicRefFactoryInternal lateinit var schemas: SchemaService lateinit var auditService: AuditService val customServices: ArrayList = ArrayList() protected val runOnStop: ArrayList = ArrayList() lateinit var database: Database protected var dbCloser: Runnable? = null + private lateinit var rpcFlows: List>> /** Locates and returns a service of the given type if loaded, or throws an exception if not found. */ inline fun findService() = customServices.filterIsInstance().single() @@ -250,6 +238,20 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } startMessagingService(rpcOps) installCoreFlows() + + fun Class>.isUserInvokable(): Boolean { + return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) + } + + val flows = scanForFlows() + rpcFlows = flows.filter { it.isUserInvokable() && it.isAnnotationPresent(StartableByRPC::class.java) } + + // Add any core flows here + listOf(ContractUpgradeFlow::class.java, + // TODO Remove all Cash flows from default list once they are split into separate CorDapp. + CashIssueFlow::class.java, + CashExitFlow::class.java, + CashPaymentFlow::class.java) + runOnStop += Runnable { net.stop() } _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) smm.start() @@ -305,8 +307,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. keyManagement = makeKeyManagementService() - flowLogicFactory = initialiseFlowLogicFactory() - scheduler = NodeSchedulerService(services, database, flowLogicFactory, unfinishedSchedules = busyNodeLatch) + scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) @@ -318,6 +319,51 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return tokenizableServices } + private fun scanForFlows(): List>> { + val pluginsDir = configuration.baseDirectory / "plugins" + log.info("Scanning plugins in $pluginsDir ...") + if (!pluginsDir.exists()) return emptyList() + + val pluginJars = pluginsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.toArray() + } + + if (pluginJars.isEmpty()) return emptyList() + + val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else + + fun loadFlowClass(className: String): Class>? { + return try { + // TODO Make sure this is loaded by the correct class loader + @Suppress("UNCHECKED_CAST") + Class.forName(className, false, javaClass.classLoader) as Class> + } catch (e: Exception) { + log.warn("Unable to load flow class $className", e) + null + } + } + + val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java) + .mapNotNull { loadFlowClass(it) } + .filterNot { isAbstract(it.modifiers) } + + fun URL.pluginName(): String { + return try { + Paths.get(toURI()).fileName.toString() + } catch (e: Exception) { + toString() + } + } + + flowClasses.groupBy { + scanResult.classNameToClassInfo[it.name]!!.classpathElementURLs.first() + }.forEach { url, classes -> + log.info("Found flows in plugin ${url.pluginName()}: ${classes.joinToString { it.name }}") + } + + return flowClasses + } + private fun initUploaders(storageServices: Pair) { val uploaders: List = listOf(storageServices.first.attachments as NodeAttachmentService) + customServices.filterIsInstance(AcceptsFileUpload::class.java) @@ -345,7 +391,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return advertisedServices.map { val serviceId = it.type.id val serviceName = it.name ?: configuration.myLegalName.replaceCommonName(serviceId) - val identity = obtainKeyPair(configuration.baseDirectory, serviceId + "-private-key", serviceId + "-public", serviceName).first + val identity = obtainKeyPair(serviceId, serviceName).first ServiceEntry(it, identity) } } @@ -386,26 +432,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private fun initialiseFlowLogicFactory(): FlowLogicRefFactoryInternal { - val flowWhitelist = HashMap>() - - for ((flowClass, extraArgumentTypes) in defaultFlowWhiteList) { - val argumentWhitelistClassNames = HashSet(extraArgumentTypes.map { it.name }) - flowClass.constructors.forEach { - it.parameters.mapTo(argumentWhitelistClassNames) { it.type.name } - } - flowWhitelist.merge(flowClass.name, argumentWhitelistClassNames, { x, y -> x + y }) - } - - for (plugin in pluginRegistries) { - for ((className, classWhitelist) in plugin.requiredFlows) { - flowWhitelist.merge(className, classWhitelist, { x, y -> x + y }) - } - } - - return FlowLogicRefFactoryImpl(flowWhitelist) - } - private fun makePluginServices(tokenizableServices: MutableList): List { val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) } tokenizableServices.addAll(pluginServices) @@ -464,7 +490,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, 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) + val request = NetworkMapService.RegistrationRequest(reg.toWire(keyManagement, legalIdentityKey.public), net.myAddress) return net.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } @@ -561,39 +587,56 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) - 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 + protected fun obtainLegalIdentity(): Party = obtainKeyPair().first + protected fun obtainLegalIdentityKey(): KeyPair = obtainKeyPair().second - private fun obtainKeyPair(dir: Path, privateKeyFileName: String, publicKeyFileName: String, serviceName: X500Name? = null): Pair { + private fun obtainKeyPair(serviceId: String = "identity", serviceName: X500Name = configuration.myLegalName): Pair { // Load the private identity key, creating it if necessary. The identity key is a long term well known key that // is distributed to other peers and we use it (or a key signed by it) when we need to do something // "permissioned". The identity file is what gets distributed and contains the node's legal name along with // the public key. Obviously in a real system this would need to be a certificate chain of some kind to ensure // the legal name is actually validated in some way. - val privKeyFile = dir / privateKeyFileName - val pubIdentityFile = dir / publicKeyFileName - val identityPrincipal: X500Name = serviceName ?: configuration.myLegalName - val identityAndKey = if (!privKeyFile.exists()) { - log.info("Identity key not found, generating fresh key!") - val keyPair: KeyPair = generateKeyPair() - keyPair.serialize().writeToFile(privKeyFile) - val myIdentity = Party(identityPrincipal, keyPair.public) - // We include the Party class with the file here to help catch mixups when admins provide files of the - // wrong type by mistake. - myIdentity.serialize().writeToFile(pubIdentityFile) - Pair(myIdentity, keyPair) - } else { + // TODO: Integrate with Key management service? + val keystore = KeyStoreUtilities.loadKeyStore(configuration.keyStoreFile, configuration.keyStorePassword) + val privateKeyAlias = "$serviceId-private-key" + val privKeyFile = configuration.baseDirectory / privateKeyAlias + val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" + + val identityAndKey = if (configuration.keyStoreFile.exists() && keystore.containsAlias(privateKeyAlias)) { + // Get keys from keystore. + val (cert, keyPair) = keystore.getCertificateAndKeyPair(privateKeyAlias, configuration.keyStorePassword) + val loadedServiceName = X509CertificateHolder(cert.encoded).subject + if (X509CertificateHolder(cert.encoded).subject != serviceName) { + throw ConfigurationException("The legal name in the config file doesn't match the stored identity keystore:" + + "$serviceName vs $loadedServiceName") + } + Pair(Party(loadedServiceName, keyPair.public), keyPair) + } else if (privKeyFile.exists()) { + // Get keys from key file. + // TODO: this is here to smooth out the key storage transition, remove this in future release. // Check that the identity in the config file matches the identity file we have stored to disk. // This is just a sanity check. It shouldn't fail unless the admin has fiddled with the files and messed // things up for us. val myIdentity = pubIdentityFile.readAll().deserialize() - if (myIdentity.name != identityPrincipal) + if (myIdentity.name != serviceName) throw ConfigurationException("The legal name in the config file doesn't match the stored identity file:" + - "$identityPrincipal vs ${myIdentity.name}") + "$serviceName vs ${myIdentity.name}") // Load the private key. val keyPair = privKeyFile.readAll().deserialize() + // TODO: Use a proper certificate chain. + val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) + keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) + keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) Pair(myIdentity, keyPair) + } else { + // Create new keys and store in keystore. + log.info("Identity key not found, generating fresh key!") + val keyPair: KeyPair = generateKeyPair() + val selfSignCert = X509Utilities.createSelfSignedCACert(serviceName, keyPair) + keystore.addOrReplaceKey(privateKeyAlias, selfSignCert.keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(selfSignCert.certificate)) + keystore.save(configuration.keyStoreFile, configuration.keyStorePassword) + Pair(Party(serviceName, selfSignCert.keyPair.public), selfSignCert.keyPair) } partyKeys += identityAndKey.second return identityAndKey diff --git a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt index 8f16f60028..acdc90af16 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -7,6 +7,7 @@ import net.corda.core.contracts.UpgradedContract import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -20,6 +21,7 @@ import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.messaging.getRpcContext 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 import net.corda.node.utilities.transaction import org.bouncycastle.asn1.x500.X500Name @@ -39,8 +41,6 @@ class CordaRPCOpsImpl( private val smm: StateMachineManager, private val database: Database ) : CordaRPCOps { - override val protocolVersion: Int = 0 - override fun networkMapUpdates(): Pair, Observable> { return database.transaction { services.networkMapCache.track() @@ -115,12 +115,8 @@ class CordaRPCOpsImpl( } } - // TODO: Check that this flow is annotated as being intended for RPC invocation override fun startTrackedFlowDynamic(logicType: Class>, vararg args: Any?): FlowProgressHandle { - val rpcContext = getRpcContext() - rpcContext.requirePermission(startFlowPermission(logicType)) - val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) - val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args) + val stateMachine = startFlow(logicType, args) return FlowProgressHandleImpl( id = stateMachine.id, returnValue = stateMachine.resultFuture, @@ -128,13 +124,17 @@ class CordaRPCOpsImpl( ) } - // TODO: Check that this flow is annotated as being intended for RPC invocation override fun startFlowDynamic(logicType: Class>, vararg args: Any?): FlowHandle { + val stateMachine = startFlow(logicType, args) + return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + } + + private fun startFlow(logicType: Class>, args: Array): FlowStateMachineImpl { + require(logicType.isAnnotationPresent(StartableByRPC::class.java)) { "${logicType.name} was not designed for RPC" } val rpcContext = getRpcContext() rpcContext.requirePermission(startFlowPermission(logicType)) val currentUser = FlowInitiator.RPC(rpcContext.currentUser.username) - val stateMachine = services.invokeFlowAsync(logicType, currentUser, *args) - return FlowHandleImpl(id = stateMachine.id, returnValue = stateMachine.resultFuture) + return services.invokeFlowAsync(logicType, currentUser, *args) } override fun attachmentExists(id: SecureHash): Boolean { @@ -171,11 +171,12 @@ class CordaRPCOpsImpl( override fun waitUntilRegisteredWithNetworkMap() = services.networkMapCache.mapServiceRegistered override fun partyFromKey(key: PublicKey) = services.identityService.partyFromKey(key) + @Suppress("DEPRECATION") @Deprecated("Use partyFromX500Name instead") override fun partyFromName(name: String) = services.identityService.partyFromName(name) override fun partyFromX500Name(x500Name: X500Name)= services.identityService.partyFromX500Name(x500Name) - override fun registeredFlows(): List = services.flowLogicRefFactory.flowWhitelist.keys.sorted() + override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() companion object { private fun stateMachineInfoFromFlowLogic(flowLogic: FlowLogic<*>): StateMachineInfo { diff --git a/node/src/main/kotlin/net/corda/node/internal/Node.kt b/node/src/main/kotlin/net/corda/node/internal/Node.kt index d20b64f167..c2847fc740 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -7,19 +7,24 @@ import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture import net.corda.core.flatMap import net.corda.core.messaging.RPCOps +import net.corda.core.minutes import net.corda.core.node.ServiceHub import net.corda.core.node.VersionInfo 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.seconds import net.corda.core.success import net.corda.core.utilities.loggerFor +import net.corda.core.utilities.trace import net.corda.node.printBasicNodeInfo import net.corda.node.serialization.NodeClock import net.corda.node.services.RPCUserService import net.corda.node.services.RPCUserServiceImpl import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.messaging.ArtemisMessagingServer +import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectRequestProperty +import net.corda.node.services.messaging.ArtemisMessagingServer.Companion.ipDetectResponseProperty import net.corda.node.services.messaging.MessagingService import net.corda.node.services.messaging.NodeMessagingClient import net.corda.node.services.transactions.PersistentUniquenessProvider @@ -28,10 +33,19 @@ import net.corda.node.services.transactions.RaftUniquenessProvider import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.node.utilities.AddressUtils import net.corda.node.utilities.AffinityExecutor +import net.corda.nodeapi.ArtemisMessagingComponent.Companion.IP_REQUEST_PREFIX +import net.corda.nodeapi.ArtemisMessagingComponent.Companion.PEER_USER import net.corda.nodeapi.ArtemisMessagingComponent.NetworkMapAddress +import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.ConnectionDirection +import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException +import org.apache.activemq.artemis.api.core.client.ActiveMQClient +import org.apache.activemq.artemis.api.core.client.ClientMessage import org.bouncycastle.asn1.x500.X500Name import org.slf4j.Logger +import java.io.IOException import java.time.Clock +import java.util.* import javax.management.ObjectName import kotlin.concurrent.thread @@ -105,6 +119,7 @@ class Node(override val configuration: FullNodeConfiguration, override fun makeMessagingService(): MessagingService { userService = RPCUserServiceImpl(configuration.rpcUsers) val serverAddress = configuration.messagingServerAddress ?: makeLocalMessageBroker() + val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null return NodeMessagingClient( configuration, @@ -114,7 +129,8 @@ class Node(override val configuration: FullNodeConfiguration, serverThread, database, networkMapRegistrationFuture, - services.monitoringService) + services.monitoringService, + configuration.messagingServerAddress == null) } private fun makeLocalMessageBroker(): HostAndPort { @@ -128,25 +144,68 @@ class Node(override val configuration: FullNodeConfiguration, /** * Checks whether the specified [host] is a public IP address or hostname. If not, tries to discover the current - * machine's public IP address to be used instead. Note that it will only work if the machine is internet-facing. - * If none found, outputs a warning message. + * machine's public IP address to be used instead. It first looks through the network interfaces, and if no public IP + * is found, asks the network map service to provide it. */ private fun tryDetectIfNotPublicHost(host: String): String? { if (!AddressUtils.isPublic(host)) { val foundPublicIP = AddressUtils.tryDetectPublicIP() + if (foundPublicIP == null) { - val message = "The specified messaging host \"$host\" is private, " + - "this node will not be reachable by any other nodes outside the private network." - println("WARNING: $message") - log.warn(message) + networkMapAddress?.let { return discoverPublicHost(it.hostAndPort) } } else { - log.info("Detected public IP: $foundPublicIP. This will be used instead the provided \"$host\" as the advertised address.") + log.info("Detected public IP: ${foundPublicIP.hostAddress}. This will be used instead of the provided \"$host\" as the advertised address.") + return foundPublicIP.hostAddress } - return foundPublicIP?.hostAddress } return null } + /** + * Asks the network map service to provide this node's public IP address: + * - Connects to the network map service's message broker and creates a special IP request queue with a custom + * request id. Marks the established session with the same request id. + * - On the server side a special post-queue-creation callback is fired. It finds the session matching the request id + * encoded in the queue name. It then extracts the remote IP from the session details and posts a message containing + * it back to the queue. + * - Once the message is received the session is closed and the queue deleted. + */ + private fun discoverPublicHost(serverAddress: HostAndPort): String? { + log.trace { "Trying to detect public hostname through the Network Map Service at $serverAddress" } + val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, configuration) + val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport).apply { + initialConnectAttempts = 5 + retryInterval = 5.seconds.toMillis() + retryIntervalMultiplier = 1.5 + maxRetryInterval = 3.minutes.toMillis() + } + val clientFactory = try { + locator.createSessionFactory() + } catch (e: ActiveMQNotConnectedException) { + throw IOException("Unable to connect to the Network Map Service at $serverAddress for IP address discovery", e) + } + + val session = clientFactory.createSession(PEER_USER, PEER_USER, false, true, true, locator.isPreAcknowledge, ActiveMQClient.DEFAULT_ACK_BATCH_SIZE) + val requestId = UUID.randomUUID().toString() + session.addMetaData(ipDetectRequestProperty, requestId) + session.start() + + val queueName = "$IP_REQUEST_PREFIX$requestId" + session.createQueue(queueName, queueName, false) + + val consumer = session.createConsumer(queueName) + val artemisMessage: ClientMessage = consumer.receive(10.seconds.toMillis()) ?: + throw IOException("Did not receive a response from the Network Map Service at $serverAddress") + val publicHostAndPort = HostAndPort.fromString(artemisMessage.getStringProperty(ipDetectResponseProperty)) + log.info("Detected public address: $publicHostAndPort") + + consumer.close() + session.deleteQueue(queueName) + clientFactory.close() + + return publicHostAndPort.host.removePrefix("/") + } + override fun startMessagingService(rpcOps: RPCOps) { // Start up the embedded MQ server messageBroker?.apply { diff --git a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt index 354b97d438..33658c855c 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -115,7 +115,7 @@ class ContractUpgradeHandler(otherSide: Party) : AbstractStateReplacementFlow.Ac val proposedTx = proposal.stx.tx val expectedTx = ContractUpgradeFlow.assembleBareTx(oldStateAndRef, proposal.modification).toWireTransaction() requireThat { - "The instigator is one of the participants" using (otherSide.owningKey in oldStateAndRef.state.data.participants) + "The instigator is one of the participants" using (otherSide in oldStateAndRef.state.data.participants) "The proposed upgrade ${proposal.modification.javaClass} is a trusted upgrade path" using (proposal.modification == authorisedUpgrade) "The proposed tx matches the expected tx for this upgrade" using (proposedTx == expectedTx) } diff --git a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt index a67796711a..e1af4b173a 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/ServiceHubInternal.kt @@ -2,7 +2,9 @@ package net.corda.node.services.api import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.flows.* +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.FlowStateMachine import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub @@ -13,6 +15,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor import net.corda.node.internal.ServiceFlowInfo import net.corda.node.services.messaging.MessagingService +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl interface NetworkMapCacheInternal : NetworkMapCache { @@ -47,11 +50,6 @@ interface NetworkMapCacheInternal : NetworkMapCache { } -interface FlowLogicRefFactoryInternal : FlowLogicRefFactory { - val flowWhitelist: Map> - fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> -} - @CordaSerializable sealed class NetworkCacheError : Exception() { /** Indicates a failure to deregister, because of a rejected request from the remote node */ @@ -64,12 +62,11 @@ abstract class ServiceHubInternal : PluginServiceHub { } abstract val monitoringService: MonitoringService - abstract val flowLogicRefFactory: FlowLogicRefFactoryInternal abstract val schemaService: SchemaService abstract override val networkMapCache: NetworkMapCacheInternal abstract val schedulerService: SchedulerService abstract val auditService: AuditService - + abstract val rpcFlows: List>> abstract val networkService: MessagingService /** @@ -119,9 +116,9 @@ abstract class ServiceHubInternal : PluginServiceHub { logicType: Class>, flowInitiator: FlowInitiator, vararg args: Any?): FlowStateMachineImpl { - val logicRef = flowLogicRefFactory.create(logicType, *args) + val logicRef = FlowLogicRefFactoryImpl.createForRPC(logicType, *args) @Suppress("UNCHECKED_CAST") - val logic = flowLogicRefFactory.toFlowLogic(logicRef) as FlowLogic + val logic = FlowLogicRefFactoryImpl.toFlowLogic(logicRef) as FlowLogic return startFlow(logic, flowInitiator) } diff --git a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt index 8eea06c609..ded16966be 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/ConfigUtilities.kt @@ -18,21 +18,23 @@ import net.corda.nodeapi.config.SSLConfiguration import org.bouncycastle.asn1.x500.X500Name import java.nio.file.Path +fun configOf(vararg pairs: Pair) = ConfigFactory.parseMap(mapOf(*pairs)) +operator fun Config.plus(overrides: Map) = ConfigFactory.parseMap(overrides).withFallback(this) + object ConfigHelper { private val log = loggerFor() fun loadConfig(baseDirectory: Path, configFile: Path = baseDirectory / "node.conf", allowMissingConfig: Boolean = false, - configOverrides: Map = emptyMap()): Config { + configOverrides: Config = ConfigFactory.empty()): Config { val parseOptions = ConfigParseOptions.defaults() val defaultConfig = ConfigFactory.parseResources("reference.conf", parseOptions.setAllowMissing(false)) val appConfig = ConfigFactory.parseFile(configFile.toFile(), parseOptions.setAllowMissing(allowMissingConfig)) - val overrideConfig = ConfigFactory.parseMap(configOverrides + mapOf( + val finalConfig = configOf( // Add substitution values here "basedir" to baseDirectory.toString()) - ) - val finalConfig = overrideConfig + .withFallback(configOverrides) .withFallback(appConfig) .withFallback(defaultConfig) .resolve() diff --git a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt index 88d8c3032d..6faa38d735 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/NodeSchedulerService.kt @@ -12,9 +12,9 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.then import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.services.api.FlowLogicRefFactoryInternal import net.corda.node.services.api.SchedulerService import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.utilities.* import org.apache.activemq.artemis.utils.ReusableLatch import org.jetbrains.exposed.sql.Database @@ -38,14 +38,12 @@ import javax.annotation.concurrent.ThreadSafe * but that starts to sound a lot like off-ledger state. * * @param services Core node services. - * @param flowLogicRefFactory Factory for restoring [FlowLogic] instances from references. * @param schedulerTimerExecutor The executor the scheduler blocks on waiting for the clock to advance to the next * activity. Only replace this for unit testing purposes. This is not the executor the [FlowLogic] is launched on. */ @ThreadSafe class NodeSchedulerService(private val services: ServiceHubInternal, private val database: Database, - private val flowLogicRefFactory: FlowLogicRefFactoryInternal, private val schedulerTimerExecutor: Executor = Executors.newSingleThreadExecutor(), private val unfinishedSchedules: ReusableLatch = ReusableLatch()) : SchedulerService, SingletonSerializeAsToken() { @@ -192,7 +190,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, ScheduledStateRef(scheduledState.ref, scheduledActivity.scheduledAt) } else { // TODO: FlowLogicRefFactory needs to sort out the class loader etc - val flowLogic = flowLogicRefFactory.toFlowLogic(scheduledActivity.logicRef) + val flowLogic = FlowLogicRefFactoryImpl.toFlowLogic(scheduledActivity.logicRef) log.trace { "Scheduler starting FlowLogic $flowLogic" } scheduledFlow = flowLogic null @@ -213,7 +211,7 @@ class NodeSchedulerService(private val services: ServiceHubInternal, val state = txState.data as SchedulableState return try { // This can throw as running contract code. - state.nextScheduledActivity(scheduledState.ref, flowLogicRefFactory) + state.nextScheduledActivity(scheduledState.ref, FlowLogicRefFactoryImpl) } catch (e: Exception) { log.error("Attempt to run scheduled state $scheduledState resulted in error.", e) null diff --git a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt index 8b8f7beec0..3509fa2d34 100644 --- a/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt +++ b/node/src/main/kotlin/net/corda/node/services/events/ScheduledActivityObserver.kt @@ -4,8 +4,8 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.SchedulableState import net.corda.core.contracts.ScheduledStateRef import net.corda.core.contracts.StateAndRef -import net.corda.core.flows.FlowLogicRefFactory import net.corda.node.services.api.ServiceHubInternal +import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl /** * This observes the vault and schedules and unschedules activities appropriately based on state production and @@ -13,16 +13,16 @@ 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.ref) } - update.produced.forEach { scheduleStateActivity(it, services.flowLogicRefFactory) } + services.vaultService.rawUpdates.subscribe { (consumed, produced) -> + consumed.forEach { services.schedulerService.unscheduleStateActivity(it.ref) } + produced.forEach { scheduleStateActivity(it) } } } - private fun scheduleStateActivity(produced: StateAndRef, flowLogicRefFactory: FlowLogicRefFactory) { + private fun scheduleStateActivity(produced: StateAndRef) { val producedState = produced.state.data if (producedState is SchedulableState) { - val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, flowLogicRefFactory)?.scheduledAt } ?: return + val scheduledAt = sandbox { producedState.nextScheduledActivity(produced.ref, FlowLogicRefFactoryImpl)?.scheduledAt } ?: return services.schedulerService.scheduleStateActivity(ScheduledStateRef(produced.ref, scheduledAt)) } } diff --git a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt index 9ff067b0cc..25ebc637a4 100644 --- a/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt +++ b/node/src/main/kotlin/net/corda/node/services/identity/InMemoryIdentityService.kt @@ -1,6 +1,9 @@ package net.corda.node.services.identity import net.corda.core.contracts.PartyAndReference +import net.corda.core.contracts.requireThat +import net.corda.core.crypto.toStringShort +import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.node.services.IdentityService @@ -8,22 +11,35 @@ import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import org.bouncycastle.asn1.x500.X500Name +import java.security.InvalidAlgorithmParameterException import java.security.PublicKey +import java.security.cert.* import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe /** * Simple identity service which caches parties and provides functionality for efficient lookup. + * + * @param identities initial set of identities for the service, typically only used for unit tests. + * @param certPaths initial set of certificate paths for the service, typically only used for unit tests. */ @ThreadSafe -class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService { +class InMemoryIdentityService(identities: Iterable = emptySet(), + certPaths: Map = emptyMap()) : SingletonSerializeAsToken(), IdentityService { companion object { private val log = loggerFor() } private val keyToParties = ConcurrentHashMap() private val principalToParties = ConcurrentHashMap() + private val partyToPath = ConcurrentHashMap() + + init { + keyToParties.putAll(identities.associateBy { it.owningKey } ) + principalToParties.putAll(identities.associateBy { it.name }) + partyToPath.putAll(certPaths) + } override fun registerIdentity(party: Party) { log.trace { "Registering identity $party" } @@ -38,6 +54,38 @@ class InMemoryIdentityService : SingletonSerializeAsToken(), IdentityService { @Deprecated("Use partyFromX500Name") override fun partyFromName(name: String): Party? = principalToParties[X500Name(name)] override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal] - override fun partyFromAnonymous(party: AnonymousParty): Party? = partyFromKey(party.owningKey) + override fun partyFromAnonymous(party: AbstractParty): Party? = partyFromKey(party.owningKey) override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + + @Throws(IdentityService.UnknownAnonymousPartyException::class) + override fun assertOwnership(party: Party, anonymousParty: AnonymousParty) { + val path = partyToPath[anonymousParty] ?: throw IdentityService.UnknownAnonymousPartyException("Unknown anonymous party ${anonymousParty.owningKey.toStringShort()}") + val target = path.certificates.last() as X509Certificate + requireThat { + "Certificate path ends with \"${target.issuerX500Principal}\" expected \"${party.name}\"" using (X500Name(target.subjectX500Principal.name) == party.name) + "Certificate path ends with correct public key" using (target.publicKey == anonymousParty.owningKey) + } + // Verify there's a previous certificate in the path, which matches + val root = path.certificates.first() as X509Certificate + require(X500Name(root.issuerX500Principal.name) == party.name) { "Certificate path starts with \"${root.issuerX500Principal}\" expected \"${party.name}\"" } + } + + override fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? = partyToPath[anonymousParty] + + @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) + override fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) { + val expectedTrustAnchor = TrustAnchor(trustedRoot, null) + require(path.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } + val target = path.certificates.last() as X509Certificate + require(target.publicKey == anonymousParty.owningKey) { "Certificate path must end with anonymous party's public key" } + val validator = CertPathValidator.getInstance("PKIX") + val validatorParameters = PKIXParameters(setOf(expectedTrustAnchor)).apply { + isRevocationEnabled = false + } + val result = validator.validate(path, validatorParameters) as PKIXCertPathValidatorResult + require(result.trustAnchor == expectedTrustAnchor) + require(result.publicKey == anonymousParty.owningKey) + + partyToPath[anonymousParty] = path + } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt index 67f48ec567..6030118332 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/E2ETestKeyManagementService.kt @@ -1,7 +1,10 @@ package net.corda.node.services.keys import net.corda.core.ThreadBox +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.keys +import net.corda.core.crypto.sign import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import java.security.KeyPair @@ -38,13 +41,26 @@ class E2ETestKeyManagementService(initialKeys: Set) : SingletonSerializ } // Accessing this map clones it. - override val keys: Map get() = mutex.locked { HashMap(keys) } + override val keys: Set get() = mutex.locked { keys.keys } - override fun freshKey(): KeyPair { + override fun freshKey(): PublicKey { val keyPair = generateKeyPair() mutex.locked { keys[keyPair.public] = keyPair.private } - return keyPair + return keyPair.public + } + + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { + return mutex.locked { + val pk = publicKey.keys.first { keys.containsKey(it) } + KeyPair(pk, keys[pk]!!) + } + } + + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { + val keyPair = getSigningKeyPair(publicKey) + val signature = keyPair.sign(bytes) + return signature } } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt index a81522e4c5..643dcdf05d 100644 --- a/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt +++ b/node/src/main/kotlin/net/corda/node/services/keys/PersistentKeyManagementService.kt @@ -1,7 +1,10 @@ package net.corda.node.services.keys import net.corda.core.ThreadBox +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.generateKeyPair +import net.corda.core.crypto.keys +import net.corda.core.crypto.sign import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.utilities.* @@ -10,7 +13,6 @@ import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey -import java.util.* /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. @@ -50,13 +52,27 @@ class PersistentKeyManagementService(initialKeys: Set) : SingletonSeria } } - override val keys: Map get() = mutex.locked { HashMap(keys) } + override val keys: Set get() = mutex.locked { keys.keys } - override fun freshKey(): KeyPair { + override fun freshKey(): PublicKey { val keyPair = generateKeyPair() mutex.locked { keys[keyPair.public] = keyPair.private } - return keyPair + return keyPair.public } + + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { + return mutex.locked { + val pk = publicKey.keys.first { keys.containsKey(it) } + KeyPair(pk, keys[pk]!!) + } + } + + override fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey { + val keyPair = getSigningKeyPair(publicKey) + val signature = keyPair.sign(bytes) + return signature + } + } diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt index d8544b5e0e..9df1d8f2ed 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/ArtemisMessagingServer.kt @@ -37,6 +37,8 @@ import org.apache.activemq.artemis.core.remoting.impl.netty.NettyConnectorFactor 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.core.server.impl.RoutingContextImpl +import org.apache.activemq.artemis.core.server.impl.ServerMessageImpl import org.apache.activemq.artemis.core.settings.impl.AddressFullMessagePolicy import org.apache.activemq.artemis.core.settings.impl.AddressSettings import org.apache.activemq.artemis.spi.core.remoting.* @@ -91,6 +93,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, private val log = loggerFor() /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ @JvmStatic val MAX_FILE_SIZE = 10485760 + + val ipDetectRequestProperty = "ip-request-id" + val ipDetectResponseProperty = "ip-address" } private class InnerState { @@ -107,6 +112,7 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, */ val networkMapConnectionFuture: SettableFuture? get() = _networkMapConnectionFuture private var networkChangeHandle: Subscription? = null + private val nodeRunsNetworkMapService = config.networkMapService == null init { config.baseDirectory.expectedOnDefaultFileSystem() @@ -138,14 +144,15 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, // Artemis IO errors @Throws(IOException::class, KeyStoreException::class) private fun configureAndStartServer() { - val config = createArtemisConfig() + val artemisConfig = createArtemisConfig() val securityManager = createArtemisSecurityManager() - activeMQServer = ActiveMQServerImpl(config, securityManager).apply { + activeMQServer = ActiveMQServerImpl(artemisConfig, securityManager).apply { // Throw any exceptions which are detected during startup 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 { deployBridgesFromNewQueue(it.toString()) } + if (nodeRunsNetworkMapService) registerPostQueueCreationCallback { handleIpDetectionRequest(it.toString()) } registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } } activeMQServer.start() @@ -228,6 +235,12 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, securityRoles[RPCApi.RPC_SERVER_QUEUE_NAME] = setOf(nodeInternalRole, restrictedRole(RPC_ROLE, send = true)) // TODO remove the NODE_USER role once the webserver doesn't need it securityRoles["${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$NODE_USER.#"] = setOf(nodeInternalRole) + if (nodeRunsNetworkMapService) { + securityRoles["$IP_REQUEST_PREFIX*"] = setOf( + nodeInternalRole, + restrictedRole(PEER_ROLE, consume = true, createNonDurableQueue = true, deleteNonDurableQueue = true) + ) + } for ((username) in userService.users) { securityRoles["${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.$username.#"] = setOf( nodeInternalRole, @@ -425,6 +438,31 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, _networkMapConnectionFuture!!.set(Unit) } } + + private fun handleIpDetectionRequest(queueName: String) { + fun getRemoteAddress(requestId: String): String? { + val session = activeMQServer.sessions.first { + it.getMetaData(ipDetectRequestProperty) == requestId + } + return session.remotingConnection.remoteAddress + } + + fun sendResponse(remoteAddress: String?) { + val responseMessage = ServerMessageImpl(random63BitValue(), 0).apply { + putStringProperty(ipDetectResponseProperty, remoteAddress) + } + val routingContext = RoutingContextImpl(null) + val queue = activeMQServer.locateQueue(SimpleString(queueName)) + queue.route(responseMessage, routingContext) + activeMQServer.postOffice.processRoute(responseMessage, routingContext, true) + } + + if (!queueName.startsWith(IP_REQUEST_PREFIX)) return + val requestId = queueName.substringAfter(IP_REQUEST_PREFIX) + val remoteAddress = getRemoteAddress(requestId) + log.debug { "Detected remote address $remoteAddress for request $requestId" } + sendResponse(remoteAddress) + } } class VerifyingNettyConnectorFactory : NettyConnectorFactory() { diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt index d879e0b8ce..bebae60032 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/NodeMessagingClient.kt @@ -24,7 +24,10 @@ import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.OutOfProcessTransactionVerifierService import net.corda.node.utilities.* -import net.corda.nodeapi.* +import net.corda.nodeapi.ArtemisMessagingComponent +import net.corda.nodeapi.ArtemisTcpTransport +import net.corda.nodeapi.ConnectionDirection +import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME import net.corda.nodeapi.VerifierApi.VERIFICATION_RESPONSES_QUEUE_NAME_PREFIX import org.apache.activemq.artemis.api.core.ActiveMQObjectClosedException @@ -58,10 +61,11 @@ import javax.annotation.concurrent.ThreadSafe * invoke methods on the provided implementation. There is more documentation on this in the docsite and the * CordaRPCClient class. * - * @param serverHostPort The address of the broker instance to connect to (might be running in the same process) + * @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" + * that this is a NetworkMapService node which will be bound globally to the name "networkmap". * @param nodeExecutor An executor to run received message tasks upon. + * @param isServerLocal Specify `true` if the provided [serverHostPort] is a locally running broker instance. */ @ThreadSafe class NodeMessagingClient(override val config: NodeConfiguration, @@ -71,7 +75,8 @@ class NodeMessagingClient(override val config: NodeConfiguration, val nodeExecutor: AffinityExecutor.ServiceAffinityExecutor, val database: Database, val networkMapRegistrationFuture: ListenableFuture, - val monitoringService: MonitoringService + val monitoringService: MonitoringService, + val isServerLocal: Boolean = true ) : ArtemisMessagingComponent(), MessagingService { companion object { private val log = loggerFor() @@ -153,10 +158,16 @@ class NodeMessagingClient(override val config: NodeConfiguration, check(!started) { "start can't be called twice" } started = true - log.info("Connecting to server: $serverHostPort") + val serverAddress = getBrokerAddress() + + log.info("Connecting to server: $serverAddress") // TODO Add broker CN to config for host verification in case the embedded broker isn't used - val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverHostPort, config) + val tcpTransport = ArtemisTcpTransport.tcpTransport(ConnectionDirection.Outbound(), serverAddress, config) val locator = ActiveMQClient.createServerLocatorWithoutHA(tcpTransport) + // 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. + locator.connectionTTL = -1 + locator.clientFailureCheckPeriod = -1 locator.minLargeMessageSize = ArtemisMessagingServer.MAX_FILE_SIZE sessionFactory = locator.createSessionFactory() @@ -205,6 +216,13 @@ class NodeMessagingClient(override val config: NodeConfiguration, resumeMessageRedelivery() } + /** + * If the message broker is running locally and [serverHostPort] specifies a public IP, the messaging client will + * fail to connect on nodes under a NAT with no loopback support. As the local message broker is listening on + * all interfaces it is safer to always use `localhost` instead. + */ + private fun getBrokerAddress() = if (isServerLocal) HostAndPort.fromParts("localhost", serverHostPort.port) else serverHostPort + /** * We make the consumer twice, once to filter for just network map messages, and then once that is complete, we close * the original and make another without a filter. We do this so that there is a network map in place for all other diff --git a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt index c7101e0d23..81935366a9 100644 --- a/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt +++ b/node/src/main/kotlin/net/corda/node/services/messaging/RPCServer.kt @@ -13,7 +13,6 @@ import com.google.common.collect.Multimaps import com.google.common.collect.SetMultimap import com.google.common.util.concurrent.ThreadFactoryBuilder import net.corda.core.ErrorOr -import net.corda.core.crypto.commonName import net.corda.core.messaging.RPCOps import net.corda.core.random63BitValue import net.corda.core.seconds @@ -42,10 +41,8 @@ import rx.Subscriber import rx.Subscription import java.lang.reflect.InvocationTargetException import java.time.Duration -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.ScheduledFuture -import java.util.concurrent.TimeUnit +import java.util.* +import java.util.concurrent.* data class RPCServerConfiguration( /** The number of threads to use for handling RPC requests */ @@ -101,22 +98,11 @@ class RPCServer( // A mapping from client addresses to IDs of associated Observables private val clientAddressToObservables = Multimaps.synchronizedSetMultimap(HashMultimap.create()) // The scheduled reaper handle. - private lateinit var reaperScheduledFuture: ScheduledFuture<*> + private var reaperScheduledFuture: ScheduledFuture<*>? = null - private val observationSendExecutor = Executors.newFixedThreadPool( - 1, - ThreadFactoryBuilder().setNameFormat("rpc-observation-sender-%d").build() - ) - - private val rpcExecutor = Executors.newScheduledThreadPool( - rpcConfiguration.rpcThreadPoolSize, - ThreadFactoryBuilder().setNameFormat("rpc-server-handler-pool-%d").build() - ) - - private val reaperExecutor = Executors.newScheduledThreadPool( - 1, - ThreadFactoryBuilder().setNameFormat("rpc-server-reaper-%d").build() - ) + private var observationSendExecutor: ExecutorService? = null + private var rpcExecutor: ScheduledExecutorService? = null + private var reaperExecutor: ScheduledExecutorService? = null private val sessionAndConsumers = ArrayList(rpcConfiguration.consumerPoolSize) private val sessionAndProducerPool = LazyStickyPool(rpcConfiguration.producerPoolBound) { @@ -125,8 +111,8 @@ class RPCServer( session.start() ArtemisProducer(sessionFactory, session, session.createProducer()) } - private lateinit var clientBindingRemovalConsumer: ClientConsumer - private lateinit var serverControl: ActiveMQServerControl + private var clientBindingRemovalConsumer: ClientConsumer? = null + private var serverControl: ActiveMQServerControl? = null private fun createObservableSubscriptionMap(): ObservableSubscriptionMap { val onObservableRemove = RemovalListener { @@ -137,52 +123,64 @@ class RPCServer( } fun start(activeMqServerControl: ActiveMQServerControl) { - log.info("Starting RPC server with configuration $rpcConfiguration") - reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate( - this::reapSubscriptions, - rpcConfiguration.reapInterval.toMillis(), - rpcConfiguration.reapInterval.toMillis(), - TimeUnit.MILLISECONDS - ) - val sessions = ArrayList() - for (i in 1 .. rpcConfiguration.consumerPoolSize) { - val sessionFactory = serverLocator.createSessionFactory() - val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) - val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) - consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) - sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) - sessions.add(session) - } - clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) - clientBindingRemovalConsumer.setMessageHandler(this::bindingRemovalArtemisMessageHandler) - serverControl = activeMqServerControl - lifeCycle.transition(State.UNSTARTED, State.STARTED) - // We delay the consumer session start because Artemis starts delivering messages immediately, so we need to be - // fully initialised. - sessions.forEach { - it.start() + try { + lifeCycle.requireState(State.UNSTARTED) + log.info("Starting RPC server with configuration $rpcConfiguration") + observationSendExecutor = Executors.newFixedThreadPool( + 1, + ThreadFactoryBuilder().setNameFormat("rpc-observation-sender-%d").build() + ) + rpcExecutor = Executors.newScheduledThreadPool( + rpcConfiguration.rpcThreadPoolSize, + ThreadFactoryBuilder().setNameFormat("rpc-server-handler-pool-%d").build() + ) + reaperExecutor = Executors.newScheduledThreadPool( + 1, + ThreadFactoryBuilder().setNameFormat("rpc-server-reaper-%d").build() + ) + reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( + this::reapSubscriptions, + rpcConfiguration.reapInterval.toMillis(), + rpcConfiguration.reapInterval.toMillis(), + TimeUnit.MILLISECONDS + ) + val sessions = ArrayList() + for (i in 1 .. rpcConfiguration.consumerPoolSize) { + val sessionFactory = serverLocator.createSessionFactory() + val session = sessionFactory.createSession(rpcServerUsername, rpcServerPassword, false, true, true, false, DEFAULT_ACK_BATCH_SIZE) + val consumer = session.createConsumer(RPCApi.RPC_SERVER_QUEUE_NAME) + consumer.setMessageHandler(this@RPCServer::clientArtemisMessageHandler) + sessionAndConsumers.add(ArtemisConsumer(sessionFactory, session, consumer)) + sessions.add(session) + } + clientBindingRemovalConsumer = sessionAndConsumers[0].session.createConsumer(RPCApi.RPC_CLIENT_BINDING_REMOVALS) + clientBindingRemovalConsumer!!.setMessageHandler(this::bindingRemovalArtemisMessageHandler) + serverControl = activeMqServerControl + lifeCycle.transition(State.UNSTARTED, State.STARTED) + // We delay the consumer session start because Artemis starts delivering messages immediately, so we need to be + // fully initialised. + sessions.forEach { + it.start() + } + } catch (exception: Throwable) { + close() + throw exception } } fun close() { - reaperScheduledFuture.cancel(false) - rpcExecutor.shutdownNow() - reaperExecutor.shutdownNow() - rpcExecutor.awaitTermination(500, TimeUnit.MILLISECONDS) - reaperExecutor.awaitTermination(500, TimeUnit.MILLISECONDS) + reaperScheduledFuture?.cancel(false) + rpcExecutor?.shutdownNow() + reaperExecutor?.shutdownNow() sessionAndConsumers.forEach { - it.consumer.close() - it.session.close() it.sessionFactory.close() } observableMap.invalidateAll() reapSubscriptions() sessionAndProducerPool.close().forEach { - it.producer.close() - it.session.close() it.sessionFactory.close() } - lifeCycle.transition(State.STARTED, State.FINISHED) + lifeCycle.justTransition(State.FINISHED) } private fun bindingRemovalArtemisMessageHandler(artemisMessage: ClientMessage) { @@ -211,7 +209,7 @@ class RPCServer( val rpcContext = RpcContext( currentUser = getUser(artemisMessage) ) - rpcExecutor.submit { + rpcExecutor!!.submit { val result = ErrorOr.catch { try { CURRENT_RPC_CONTEXT.set(rpcContext) @@ -239,9 +237,9 @@ class RPCServer( observableMap, clientAddressToObservables, clientToServer.clientAddress, - serverControl, + serverControl!!, sessionAndProducerPool, - observationSendExecutor, + observationSendExecutor!!, kryoPool ) observableContext.sendMessage(reply) @@ -255,7 +253,6 @@ class RPCServer( } private fun reapSubscriptions() { - lifeCycle.requireState(State.STARTED) observableMap.cleanUp() } diff --git a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt index bdc2162c7a..144223d4ca 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/NetworkMapService.kt @@ -8,6 +8,7 @@ import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.DEFAULT_SESSION_ID +import net.corda.core.node.services.KeyManagementService import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.ServiceType import net.corda.core.random63BitValue @@ -31,7 +32,7 @@ import net.corda.node.services.network.NetworkMapService.Companion.SUBSCRIPTION_ import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.AddOrRemove.ADD import net.corda.node.utilities.AddOrRemove.REMOVE -import java.security.PrivateKey +import java.security.PublicKey import java.security.SignatureException import java.time.Instant import java.time.Period @@ -322,9 +323,9 @@ data class NodeRegistration(val node: NodeInfo, val serial: Long, val type: AddO /** * Build a node registration in wire format. */ - fun toWire(privateKey: PrivateKey): WireNodeRegistration { + fun toWire(keyManager: KeyManagementService, publicKey: PublicKey): WireNodeRegistration { val regSerialized = this.serialize() - val regSig = privateKey.sign(regSerialized.bytes, node.legalIdentity.owningKey) + val regSig = keyManager.sign(regSerialized.bytes, publicKey) return WireNodeRegistration(regSerialized, regSig) } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt index fd2a9b3e89..150143cc34 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowLogicRefFactoryImpl.kt @@ -1,14 +1,10 @@ package net.corda.node.services.statemachine +import com.google.common.annotations.VisibleForTesting import com.google.common.primitives.Primitives -import net.corda.core.crypto.SecureHash -import net.corda.core.flows.AppContext -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowLogicRef -import net.corda.core.flows.IllegalFlowLogicException +import net.corda.core.flows.* import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken -import net.corda.node.services.api.FlowLogicRefFactoryInternal import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import java.util.* @@ -29,59 +25,26 @@ data class FlowLogicRefImpl internal constructor(val flowLogicClassName: String, * Validation of types is performed on the way in and way out in case this object is passed between JVMs which might have differing * whitelists. * - * TODO: Ways to populate whitelist of "blessed" flows per node/party - * TODO: Ways to populate argument types whitelist. Per node/party or global? * TODO: Align with API related logic for passing in FlowLogic references (FlowRef) * TODO: Actual support for AppContext / AttachmentsClassLoader + * TODO: at some point check whether there is permission, beyond the annotations, to start flows. For example, as a security + * measure we might want the ability for the node admin to blacklist a flow such that it moves immediately to the "Flow Hospital" + * in response to a potential malicious use or buggy update to an app etc. */ -class FlowLogicRefFactoryImpl(override val flowWhitelist: Map>) : SingletonSerializeAsToken(), FlowLogicRefFactoryInternal { - constructor() : this(mapOf()) - - // Pending real dependence on AppContext for class loading etc - @Suppress("UNUSED_PARAMETER") - private fun validateFlowClassName(className: String, appContext: AppContext) { - // TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check - require(flowWhitelist.containsKey(className)) { "${FlowLogic::class.java.simpleName} of ${FlowLogicRef::class.java.simpleName} must have type on the whitelist: $className" } - } - - // Pending real dependence on AppContext for class loading etc - @Suppress("UNUSED_PARAMETER") - private fun validateArgClassName(className: String, argClassName: String, appContext: AppContext) { - // TODO: consider more carefully what to whitelist and how to secure flows - // For now automatically accept standard java.lang.* and kotlin.* types. - // All other types require manual specification at FlowLogicRefFactory construction time. - if (argClassName.startsWith("java.lang.") || argClassName.startsWith("kotlin.")) { - return +object FlowLogicRefFactoryImpl : SingletonSerializeAsToken(), FlowLogicRefFactory { + override fun create(flowClass: Class>, vararg args: Any?): FlowLogicRef { + if (!flowClass.isAnnotationPresent(SchedulableFlow::class.java)) { + throw IllegalFlowLogicException(flowClass, "because it's not a schedulable flow") } - // TODO: make this specific to the attachments in the [AppContext] by including [SecureHash] in whitelist check - require(flowWhitelist[className]!!.contains(argClassName)) { "Args to $className must have types on the args whitelist: $argClassName, but it has ${flowWhitelist[className]}" } + return createForRPC(flowClass, *args) } - /** - * Create a [FlowLogicRef] for the Kotlin primary constructor of a named [FlowLogic] - */ - fun createKotlin(flowLogicClassName: String, args: Map, attachments: List = emptyList()): FlowLogicRef { - val context = AppContext(attachments) - validateFlowClassName(flowLogicClassName, context) - for (arg in args.values.filterNotNull()) { - validateArgClassName(flowLogicClassName, arg.javaClass.name, context) - } - val clazz = Class.forName(flowLogicClassName) - require(FlowLogic::class.java.isAssignableFrom(clazz)) { "$flowLogicClassName is not a FlowLogic" } - @Suppress("UNCHECKED_CAST") - val logic = clazz as Class>> - return createKotlin(logic, args) - } - - /** - * Create a [FlowLogicRef] by assuming a single constructor and the given args. - */ - override fun create(type: Class>, vararg args: Any?): FlowLogicRef { + fun createForRPC(flowClass: Class>, vararg args: Any?): FlowLogicRef { // TODO: This is used via RPC but it's probably better if we pass in argument names and values explicitly // to avoid requiring only a single constructor. val argTypes = args.map { it?.javaClass } val constructor = try { - type.kotlin.constructors.single { ctor -> + flowClass.kotlin.constructors.single { ctor -> // Get the types of the arguments, always boxed (as that's what we get in the invocation). val ctorTypes = ctor.javaConstructor!!.parameterTypes.map { Primitives.wrap(it) } if (argTypes.size != ctorTypes.size) @@ -93,14 +56,14 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map>, args: Map): FlowLogicRef { + @VisibleForTesting + internal fun createKotlin(type: Class>, args: Map): FlowLogicRef { // TODO: we need to capture something about the class loader or "application context" into the ref, // perhaps as some sort of ThreadLocal style object. For now, just create an empty one. val appContext = AppContext(emptyList()) - validateFlowClassName(type.name, appContext) // Check we can find a constructor and populate the args to it, but don't call it - createConstructor(appContext, type, args) + createConstructor(type, args) return FlowLogicRefImpl(type.name, appContext, args) } - /** - * Create a [FlowLogicRef] by trying to find a Java constructor that matches the given args. - */ - private fun createJava(type: Class>, vararg args: Any?): FlowLogicRef { - // Build map for each - val argsMap = HashMap(args.size) - var index = 0 - args.forEach { argsMap["arg${index++}"] = it } - return createKotlin(type, argsMap) - } - - override fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { + fun toFlowLogic(ref: FlowLogicRef): FlowLogic<*> { if (ref !is FlowLogicRefImpl) throw IllegalFlowLogicException(ref.javaClass, "FlowLogicRef was not created via correct FlowLogicRefFactory interface") - validateFlowClassName(ref.flowLogicClassName, ref.appContext) val klass = Class.forName(ref.flowLogicClassName, true, ref.appContext.classLoader).asSubclass(FlowLogic::class.java) - return createConstructor(ref.appContext, klass, ref.args)() + return createConstructor(klass, ref.args)() } - private fun createConstructor(appContext: AppContext, clazz: Class>, args: Map): () -> FlowLogic<*> { + private fun createConstructor(clazz: Class>, args: Map): () -> FlowLogic<*> { for (constructor in clazz.kotlin.constructors) { - val params = buildParams(appContext, clazz, constructor, args) ?: continue + val params = buildParams(constructor, args) ?: continue // If we get here then we matched every parameter return { constructor.callBy(params) } } throw IllegalFlowLogicException(clazz, "as could not find matching constructor for: $args") } - private fun buildParams(appContext: AppContext, clazz: Class>, constructor: KFunction>, args: Map): HashMap? { + private fun buildParams(constructor: KFunction>, args: Map): HashMap? { val params = hashMapOf() val usedKeys = hashSetOf() for (parameter in constructor.parameters) { @@ -159,7 +110,6 @@ class FlowLogicRefFactoryImpl(override val flowWhitelist: Map(override val id: StateMachineRunId, // Check if the FlowException was propagated by looking at where the stack trace originates (see suspendAndExpectReceive). val propagated = e.stackTrace[0].className == javaClass.name processException(e, propagated) - logger.error(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e) + logger.warn(if (propagated) "Flow ended due to receiving exception" else "Flow finished with exception", e) return } catch (t: Throwable) { recordDuration(startTime, success = false) diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt index 2d9e19cb82..9e249ef0f0 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/StateMachineManager.kt @@ -97,7 +97,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, init { Fiber.setDefaultUncaughtExceptionHandler { fiber, throwable -> - (fiber as FlowStateMachineImpl<*>).logger.error("Caught exception from flow", throwable) + (fiber as FlowStateMachineImpl<*>).logger.warn("Caught exception from flow", throwable) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt index ca99b8a7bb..90a69ce161 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRt.kt @@ -9,7 +9,10 @@ import bftsmart.tom.server.defaultservices.DefaultReplier import bftsmart.tom.util.Extractor import net.corda.core.contracts.StateRef import net.corda.core.contracts.Timestamp -import net.corda.core.crypto.* +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.SignedData +import net.corda.core.crypto.sign import net.corda.core.identity.Party import net.corda.core.node.services.TimestampChecker import net.corda.core.node.services.UniquenessProvider @@ -198,8 +201,7 @@ object BFTSMaRt { } protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { - val mySigningKey = db.transaction { services.notaryIdentityKey } - return mySigningKey.sign(bytes) + return db.transaction { services.keyManagementService.sign(bytes, services.notaryIdentityKey) } } // TODO: diff --git a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt index d86b8c7ce0..75f20fec92 100644 --- a/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt +++ b/node/src/main/kotlin/net/corda/node/services/vault/NodeVaultService.kt @@ -11,11 +11,12 @@ import io.requery.kotlin.notNull import io.requery.query.RowExpression import net.corda.contracts.asset.Cash import net.corda.contracts.asset.OnLedgerAsset -import net.corda.contracts.clause.AbstractConserveAmount import net.corda.core.ThreadBox import net.corda.core.bufferUntilSubscribed import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.containsAny +import net.corda.core.crypto.toBase58String import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.node.ServiceHub @@ -242,7 +243,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P } override fun notifyAll(txns: Iterable) { - val ourKeys = services.keyManagementService.keys.keys + val ourKeys = services.keyManagementService.keys val netDelta = txns.fold(Vault.NoUpdate) { netDelta, txn -> netDelta + makeUpdate(txn, ourKeys) } if (netDelta != Vault.NoUpdate) { recordUpdate(netDelta) @@ -468,7 +469,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P @Suspendable override fun generateSpend(tx: TransactionBuilder, amount: Amount, - to: PublicKey, + to: AbstractParty, onlyFromParties: Set?): Pair> { // Retrieve unspent and unlocked cash states that meet our spending criteria. val acceptableCoins = unconsumedStatesForSpending(amount, onlyFromParties, tx.notary, tx.lockId) @@ -477,7 +478,7 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P { Cash().generateMoveCommand() }) } - private fun deriveState(txState: TransactionState, amount: Amount>, owner: PublicKey) + private fun deriveState(txState: TransactionState, amount: Amount>, owner: AbstractParty) = txState.copy(data = txState.data.copy(amount = amount, owner = owner)) private fun makeUpdate(tx: WireTransaction, ourKeys: Set): Vault.Update { @@ -528,10 +529,9 @@ class NodeVaultService(private val services: ServiceHub, dataSourceProperties: P } private fun isRelevant(state: ContractState, ourKeys: Set) = when (state) { - is OwnableState -> state.owner.containsAny(ourKeys) - // It's potentially of interest to the vault + is OwnableState -> state.owner.owningKey.containsAny(ourKeys) is LinearState -> state.isRelevant(ourKeys) - else -> false + else -> ourKeys.intersect(state.participants.map { it.owningKey }).isNotEmpty() } /** diff --git a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt index 603faa42ed..6ff7a61d67 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -15,6 +15,7 @@ import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStateMachine import net.corda.core.messaging.CordaRPCOps import net.corda.core.utilities.Emoji +import net.corda.core.utilities.loggerFor import net.corda.jackson.JacksonSupport import net.corda.jackson.StringToMethodCallParser import net.corda.node.internal.Node @@ -70,6 +71,7 @@ import kotlin.concurrent.thread // TODO: Make it notice new shell commands added after the node started. object InteractiveShell { + private val log = loggerFor() private lateinit var node: Node /** @@ -129,6 +131,7 @@ object InteractiveShell { thread(name = "Command line shell terminator", isDaemon = true) { // Wait for the shell to finish. jlineProcessor.closed() + log.info("Command shell has exited") terminal.restore() node.stop() } @@ -210,7 +213,7 @@ object InteractiveShell { */ @JvmStatic fun runFlowByNameFragment(nameFragment: String, inputData: String, output: RenderPrintWriter) { - val matches = node.flowLogicFactory.flowWhitelist.keys.filter { nameFragment in it } + val matches = node.services.rpcFlows.filter { nameFragment in it.name } if (matches.isEmpty()) { output.println("No matching flow found, run 'flow list' to see your options.", Color.red) return @@ -219,14 +222,12 @@ object InteractiveShell { matches.forEachIndexed { i, s -> output.println("${i + 1}. $s", Color.yellow) } return } - val match = matches.single() - val clazz = Class.forName(match) - if (!FlowLogic::class.java.isAssignableFrom(clazz)) - throw IllegalStateException("Found a non-FlowLogic class in the whitelist? $clazz") + + @Suppress("UNCHECKED_CAST") + val clazz = matches.single() as Class> try { // TODO Flow invocation should use startFlowDynamic. - @Suppress("UNCHECKED_CAST") - val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz as Class>) + val fsm = runFlowFromString({ node.services.startFlow(it, FlowInitiator.Shell) }, inputData, clazz) // Show the progress tracker on the console until the flow completes or is interrupted with a // Ctrl-C keypress. val latch = CountDownLatch(1) @@ -275,10 +276,7 @@ object InteractiveShell { var paramNamesFromConstructor: List? = null fun getPrototype(ctor: Constructor<*>): List { val argTypes = ctor.parameterTypes.map { it.simpleName } - val prototype = paramNamesFromConstructor!!.zip(argTypes).map { pair -> - val (name, type) = pair - "$name: $type" - } + val prototype = paramNamesFromConstructor!!.zip(argTypes).map { (name, type) -> "$name: $type" } return prototype } try { diff --git a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt index 304aa3db68..31d0bce621 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -4,12 +4,12 @@ import net.corda.core.crypto.CompositeKey import net.corda.core.identity.Party import net.corda.core.crypto.generateKeyPair import net.corda.core.serialization.serialize +import net.corda.core.serialization.storageKryo import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace import org.bouncycastle.asn1.x500.X500Name import java.nio.file.Files import java.nio.file.Path -import java.nio.file.Paths object ServiceIdentityGenerator { private val log = loggerFor() @@ -36,17 +36,8 @@ object ServiceIdentityGenerator { val privateKeyFile = "$serviceId-private-key" val publicKeyFile = "$serviceId-public" notaryParty.writeToFile(dir.resolve(publicKeyFile)) - keyPair.serialize().writeToFile(dir.resolve(privateKeyFile)) + // Use storageKryo as our whitelist is not available in the gradle build environment: + keyPair.serialize(storageKryo()).writeToFile(dir.resolve(privateKeyFile)) } } } - -fun main(args: Array) { - val dirs = args[0].split("|").map { Paths.get(it) } - val serviceId = args[1] - val serviceName = X500Name(args[2]) - val quorumSize = args.getOrNull(3)?.toInt() ?: 1 - - println("Generating service identity for \"$serviceName\"") - ServiceIdentityGenerator.generateToDisk(dirs, serviceId, serviceName, quorumSize) -} \ No newline at end of file diff --git a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java index 20eb7fa812..5e0a69cbad 100644 --- a/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java +++ b/node/src/test/java/net/corda/node/services/events/FlowLogicRefFromJavaTest.java @@ -1,15 +1,9 @@ package net.corda.node.services.events; import net.corda.core.flows.FlowLogic; -import net.corda.core.flows.FlowLogicRefFactory; import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl; import org.junit.Test; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - public class FlowLogicRefFromJavaTest { @SuppressWarnings("unused") @@ -56,20 +50,11 @@ public class FlowLogicRefFromJavaTest { @Test public void test() { - Map> whiteList = new HashMap<>(); - Set argsList = new HashSet<>(); - argsList.add(ParamType1.class.getName()); - argsList.add(ParamType2.class.getName()); - whiteList.put(JavaFlowLogic.class.getName(), argsList); - FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList); - factory.create(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); + FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaFlowLogic.class, new ParamType1(1), new ParamType2("Hello Jack")); } @Test public void testNoArg() { - Map> whiteList = new HashMap<>(); - whiteList.put(JavaNoArgFlowLogic.class.getName(), new HashSet<>()); - FlowLogicRefFactory factory = new FlowLogicRefFactoryImpl(whiteList); - factory.create(JavaNoArgFlowLogic.class); + FlowLogicRefFactoryImpl.INSTANCE.createForRPC(JavaNoArgFlowLogic.class); } } diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index 36ed83a2ab..da63771508 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -1,9 +1,11 @@ package net.corda.node +import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.asset.Cash import net.corda.core.contracts.* import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.keys +import net.corda.core.flows.FlowLogic import net.corda.core.flows.StateMachineRunId import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.startFlow @@ -35,7 +37,9 @@ import org.junit.Before import org.junit.Test import rx.Observable import java.io.ByteArrayOutputStream -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue class CordaRPCOpsImplTest { @@ -87,7 +91,7 @@ class CordaRPCOpsImplTest { val expectedState = Cash.State(Amount(quantity, Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), - recipient.owningKey) + recipient) var issueSmId: StateMachineRunId? = null stateMachineUpdates.expectEvents { @@ -118,7 +122,6 @@ class CordaRPCOpsImplTest { @Test fun `issue and move`() { - rpc.startFlow(::CashIssueFlow, Amount(100, USD), OpaqueBytes(ByteArray(1, { 1 })), @@ -225,4 +228,19 @@ class CordaRPCOpsImplTest { assertArrayEquals(bufferFile.toByteArray(), bufferRpc.toByteArray()) } + + @Test + fun `attempt to start non-RPC flow`() { + CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( + startFlowPermission() + )))) + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + rpc.startFlow(::NonRPCFlow) + } + } + + class NonRPCFlow : FlowLogic() { + @Suspendable + override fun call() = Unit + } } diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt new file mode 100644 index 0000000000..8c472f2f81 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt @@ -0,0 +1,22 @@ +package net.corda.node.internal + +import net.corda.core.createDirectories +import net.corda.core.crypto.commonName +import net.corda.core.div +import net.corda.core.getOrThrow +import net.corda.core.utilities.ALICE +import net.corda.testing.node.NodeBasedTest +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class NodeTest : NodeBasedTest() { + @Test + fun `empty plugins directory`() { + val baseDirectory = tempFolder.root.toPath() / ALICE.name.commonName + (baseDirectory / "plugins").createDirectories() + val node = startNode(ALICE.name).getOrThrow() + // Make sure we created the plugins dir in the correct place + assertThat(baseDirectory).isEqualTo(node.configuration.baseDirectory) + + } +} \ No newline at end of file diff --git a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt index ab5663be20..1cde75adcf 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -5,20 +5,24 @@ import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* -import net.corda.core.identity.AnonymousParty -import net.corda.core.identity.Party +import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.sign import net.corda.core.days import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowStateMachine import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StateMachineRunId import net.corda.core.getOrThrow +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.identity.AbstractParty import net.corda.core.map import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.* import net.corda.core.rootCause +import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction @@ -66,7 +70,6 @@ class TwoPartyTradeFlowTests { @Before fun before() { net = MockNetwork(false) - net.identities += MOCK_IDENTITY_SERVICE.identities LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -86,8 +89,6 @@ class TwoPartyTradeFlowTests { val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) - val aliceKey = aliceNode.services.legalIdentityKey - val notaryKey = notaryNode.services.notaryIdentityKey aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -97,11 +98,11 @@ class TwoPartyTradeFlowTests { } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(false, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second } - insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey, notaryKey) + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()) @@ -132,8 +133,6 @@ class TwoPartyTradeFlowTests { val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) - val aliceKey = aliceNode.services.legalIdentityKey - val notaryKey = notaryNode.services.notaryIdentityKey aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -144,11 +143,11 @@ class TwoPartyTradeFlowTests { } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(false, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second } - insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey, notaryKey) + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) val cashLockId = UUID.randomUUID() bobNode.database.transaction { @@ -183,8 +182,6 @@ class TwoPartyTradeFlowTests { var bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() - val aliceKey = aliceNode.services.legalIdentityKey - val notaryKey = notaryNode.services.notaryIdentityKey val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.PeerHandle val networkMapAddr = notaryNode.info.address @@ -195,10 +192,10 @@ class TwoPartyTradeFlowTests { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, outputNotary = notaryNode.info.notaryIdentity) } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(false, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, null, notaryNode.info.notaryIdentity).second } - insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey, notaryKey) + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) val aliceFuture = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerResult // Everything is on this thread so we can now step through the flow one step at a time. @@ -301,7 +298,6 @@ class TwoPartyTradeFlowTests { val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) - val aliceKey = aliceNode.services.legalIdentityKey ledger(aliceNode.services) { @@ -317,15 +313,15 @@ class TwoPartyTradeFlowTests { } val extraKey = bobNode.keyManagement.freshKey() - val bobsFakeCash = fillUpForBuyer(false, extraKey.public, + val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(extraKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second - val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, bobNode.services.legalIdentityKey, extraKey) + val bobsSignedTxns = insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, extraKey, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(false, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, attachmentID, notaryNode.info.notaryIdentity).second } - val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey) + val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) net.runNetwork() // Clear network map registration messages @@ -401,7 +397,6 @@ class TwoPartyTradeFlowTests { val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) - val aliceKey = aliceNode.services.legalIdentityKey ledger(aliceNode.services) { @@ -416,17 +411,18 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val bobsFakeCash = fillUpForBuyer(false, bobNode.keyManagement.freshKey().public, + val bobsKey = bobNode.keyManagement.freshKey() + val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(bobsKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second - insertFakeTransactions(bobsFakeCash, bobNode, notaryNode) + insertFakeTransactions(bobsFakeCash, bobNode, notaryNode, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(false, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(false, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` DUMMY_CASH_ISSUER, attachmentID, notaryNode.info.notaryIdentity).second } - insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey) + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) net.runNetwork() // Clear network map registration messages @@ -504,6 +500,8 @@ class TwoPartyTradeFlowTests { serviceHub.legalIdentityKey)) } + sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentity) + buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentity) val buyerFuture = buyerNode.initiateSingleShotFlow(SellerRunnerFlow::class) { otherParty -> Buyer(otherParty, notaryNode.info.notaryIdentity, 1000.DOLLARS, CommercialPaper.State::class.java) }.map { it.stateMachine } @@ -520,19 +518,19 @@ class TwoPartyTradeFlowTests { val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) val bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) - val aliceKey = aliceNode.services.legalIdentityKey - val bobKey = bobNode.services.legalIdentityKey val issuer = MEGA_CORP.ref(1, 2, 3) - val bobsBadCash = fillUpForBuyer(bobError, bobKey.public, DUMMY_CASH_ISSUER.party, - notaryNode.info.notaryIdentity).second + val bobsBadCash = bobNode.database.transaction { + fillUpForBuyer(bobError, bobNode.info.legalIdentity, DUMMY_CASH_ISSUER.party, + notaryNode.info.notaryIdentity).second + } val alicesFakePaper = aliceNode.database.transaction { - fillUpForSeller(aliceError, aliceNode.info.legalIdentity.owningKey, + fillUpForSeller(aliceError, aliceNode.info.legalIdentity, 1200.DOLLARS `issued by` issuer, null, notaryNode.info.notaryIdentity).second } - insertFakeTransactions(bobsBadCash, bobNode, notaryNode, bobKey) - insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, aliceKey) + insertFakeTransactions(bobsBadCash, bobNode, notaryNode, DUMMY_CASH_ISSUER_KEY.public, MEGA_CORP_PUBKEY) + insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) net.runNetwork() // Clear network map registration messages @@ -557,8 +555,25 @@ class TwoPartyTradeFlowTests { wtxToSign: List, node: AbstractNode, notaryNode: MockNetwork.MockNode, - vararg extraKeys: KeyPair): Map { - val signed: List = signAll(wtxToSign, extraKeys.toList() + notaryNode.services.notaryIdentityKey + DUMMY_CASH_ISSUER_KEY) + vararg extraKeys: PublicKey): Map { + + val signed = wtxToSign.map { + val bits = it.serialize() + val id = it.id + val sigs = mutableListOf() + sigs.add(node.services.keyManagementService.sign(id.bytes, node.services.legalIdentityKey)) + sigs.add(notaryNode.services.keyManagementService.sign(id.bytes, notaryNode.services.notaryIdentityKey)) + for (extraKey in extraKeys) { + if (extraKey == DUMMY_CASH_ISSUER_KEY.public) { + sigs.add(DUMMY_CASH_ISSUER_KEY.sign(id.bytes)) + } else if (extraKey == MEGA_CORP_PUBKEY) { + sigs.add(MEGA_CORP_KEY.sign(id.bytes)) + } else { + sigs.add(node.services.keyManagementService.sign(id.bytes, extraKey)) + } + } + SignedTransaction(bits, sigs) + } return node.database.transaction { node.services.recordTransactions(signed) val validatedTransactions = node.services.storageService.validatedTransactions @@ -571,16 +586,16 @@ class TwoPartyTradeFlowTests { private fun LedgerDSL.fillUpForBuyer( withError: Boolean, - owner: PublicKey, - issuer: AnonymousParty, + owner: AbstractParty, + issuer: AbstractParty, notary: Party): Pair, List> { - val interimOwnerKey = MEGA_CORP_PUBKEY + val interimOwner = MEGA_CORP // Bob (Buyer) has some cash he got from the Bank of Elbonia, Alice (Seller) has some commercial paper she // wants to sell to Bob. val eb1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { // Issued money to itself. - output("elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwnerKey } - output("elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwnerKey } + output("elbonian money 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } + output("elbonian money 2", notary = notary) { 1000.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } if (!withError) { command(issuer.owningKey) { Cash.Commands.Issue() } } else { @@ -599,15 +614,15 @@ class TwoPartyTradeFlowTests { val bc1 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 1") output("bob cash 1", notary = notary) { 800.DOLLARS.CASH `issued by` issuer `owned by` owner } - command(interimOwnerKey) { Cash.Commands.Move() } + command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } val bc2 = transaction(transactionBuilder = TransactionBuilder(notary = notary)) { input("elbonian money 2") output("bob cash 2", notary = notary) { 300.DOLLARS.CASH `issued by` issuer `owned by` owner } - output(notary = notary) { 700.DOLLARS.CASH `issued by` issuer `owned by` interimOwnerKey } // Change output. - command(interimOwnerKey) { Cash.Commands.Move() } + output(notary = notary) { 700.DOLLARS.CASH `issued by` issuer `owned by` interimOwner } // Change output. + command(interimOwner.owningKey) { Cash.Commands.Move() } this.verifies() } @@ -617,7 +632,7 @@ class TwoPartyTradeFlowTests { private fun LedgerDSL.fillUpForSeller( withError: Boolean, - owner: PublicKey, + owner: AbstractParty, amount: Amount>, attachmentID: SecureHash?, notary: Party): Pair, List> { diff --git a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt index 7ac3f7038e..e79a83b653 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -12,7 +12,6 @@ import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.messaging.MessagingService import net.corda.node.services.schema.NodeSchemaService -import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.InMemoryTransactionVerifierService @@ -30,7 +29,6 @@ open class MockServiceHubInternal( val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), val scheduler: SchedulerService? = null, val overrideClock: Clock? = NodeClock(), - val flowFactory: FlowLogicRefFactoryInternal? = FlowLogicRefFactoryImpl(), val schemas: SchemaService? = NodeSchemaService(), val customTransactionVerifierService: TransactionVerifierService? = InMemoryTransactionVerifierService(2) ) : ServiceHubInternal() { @@ -56,8 +54,8 @@ open class MockServiceHubInternal( get() = throw UnsupportedOperationException() override val monitoringService: MonitoringService = MonitoringService(MetricRegistry()) - override val flowLogicRefFactory: FlowLogicRefFactoryInternal - get() = flowFactory ?: throw UnsupportedOperationException() + override val rpcFlows: List>> + get() = throw UnsupportedOperationException() override val schemaService: SchemaService get() = schemas ?: throw UnsupportedOperationException() override val auditService: AuditService = DummyAuditService() diff --git a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt index 2581b99266..c0be61f475 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -123,9 +123,9 @@ class NotaryChangeTests { 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 stateA = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) + val stateB = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) + val stateC = DummyContract.SingleOwnerState(Random().nextInt(), owner.party) val tx = TransactionType.General.Builder(null).apply { addCommand(Command(DummyContract.Commands.Create(), owner.party.owningKey)) @@ -133,9 +133,7 @@ class NotaryChangeTests { addOutputState(stateC, notary) addOutputState(stateB, notary, encumbrance = 1) // Encumbered by stateC } - val nodeKey = node.services.legalIdentityKey - tx.signWith(nodeKey) - val stx = tx.toSignedTransaction() + val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(listOf(stx)) return tx.toWireTransaction() } @@ -151,26 +149,19 @@ class NotaryChangeTests { fun issueState(node: AbstractNode, notaryNode: AbstractNode): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) - val nodeKey = node.services.legalIdentityKey - tx.signWith(nodeKey) - val notaryKeyPair = notaryNode.services.notaryIdentityKey - tx.signWith(notaryKeyPair) - val stx = tx.toSignedTransaction() + val signedByNode = node.services.signInitialTransaction(tx) + val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) node.services.recordTransactions(listOf(stx)) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: AbstractNode): StateAndRef { val state = TransactionState(DummyContract.MultiOwnerState(0, - listOf(nodeA.info.legalIdentity.owningKey, nodeB.info.legalIdentity.owningKey)), notaryNode.info.notaryIdentity) + listOf(nodeA.info.legalIdentity, nodeB.info.legalIdentity)), notaryNode.info.notaryIdentity) val tx = TransactionType.NotaryChange.Builder(notaryNode.info.notaryIdentity).withItems(state) - val nodeAKey = nodeA.services.legalIdentityKey - val nodeBKey = nodeB.services.legalIdentityKey - tx.signWith(nodeAKey) - tx.signWith(nodeBKey) - val notaryKeyPair = notaryNode.services.notaryIdentityKey - tx.signWith(notaryKeyPair) - val stx = tx.toSignedTransaction() + val signedByA = nodeA.services.signInitialTransaction(tx) + val signedByAB = nodeB.services.addSignature(signedByA) + val stx = notaryNode.services.addSignature(signedByAB, notaryNode.services.notaryIdentityKey) nodeA.services.recordTransactions(listOf(stx)) nodeB.services.recordTransactions(listOf(stx)) val stateAndRef = StateAndRef(state, StateRef(stx.id, 0)) @@ -180,9 +171,7 @@ fun issueMultiPartyState(nodeA: AbstractNode, nodeB: AbstractNode, notaryNode: A fun issueInvalidState(node: AbstractNode, notary: Party): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notary, node.info.legalIdentity.ref(0)) tx.setTime(Instant.now(), 30.seconds) - val nodeKey = node.services.legalIdentityKey - tx.signWith(nodeKey) - val stx = tx.toSignedTransaction(false) + val stx = node.services.signInitialTransaction(tx) node.services.recordTransactions(listOf(stx)) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/test/kotlin/net/corda/node/services/config/ConfigOperatorTests.kt b/node/src/test/kotlin/net/corda/node/services/config/ConfigOperatorTests.kt new file mode 100644 index 0000000000..92f7340b16 --- /dev/null +++ b/node/src/test/kotlin/net/corda/node/services/config/ConfigOperatorTests.kt @@ -0,0 +1,24 @@ +package net.corda.node.services.config + +import com.typesafe.config.ConfigFactory +import net.corda.nodeapi.config.toProperties +import org.junit.Test +import kotlin.test.assertEquals + +class ConfigOperatorTests { + + @Test + fun `config plus behaves the same as map plus`() { + val config = arrayOf("x" to "y1", "a" to "b", "z" to "Z") + val overrides = arrayOf("x" to "y2", "c" to "d", "z" to null) + val old = ConfigFactory.parseMap(mapOf(*config) + mapOf(*overrides)) + val new = configOf(*config) + mapOf(*overrides) + listOf(old, new).map { it.toProperties() }.forEach { c -> + assertEquals("y2", c["x"]) + assertEquals("b", c["a"]) + assertEquals("d", c["c"]) + assertEquals(null, c["z"]) + } + } + +} diff --git a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index f27f519738..447769ea45 100644 --- a/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt @@ -10,6 +10,7 @@ import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.NullPublicKey import net.corda.core.crypto.SecureHash import net.corda.core.crypto.toBase58String +import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.Vault import net.corda.core.serialization.serialize import net.corda.core.serialization.storageKryo @@ -129,7 +130,7 @@ class RequeryConfigurationTest { index = txnState.index stateStatus = Vault.StateStatus.UNCONSUMED contractStateClassName = DummyContract.SingleOwnerState::class.java.name - contractState = DummyContract.SingleOwnerState(owner = DUMMY_PUBKEY_1).serialize(storageKryo()).bytes + contractState = DummyContract.SingleOwnerState(owner = AnonymousParty(DUMMY_PUBKEY_1)).serialize(storageKryo()).bytes notaryName = txn.tx.notary!!.name.toString() notaryKey = txn.tx.notary!!.owningKey.toBase58String() recordedTime = Instant.now() diff --git a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt index 3baa1a7448..ae6a5b7680 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/FlowLogicRefTest.kt @@ -1,9 +1,8 @@ package net.corda.node.services.events -import net.corda.core.days import net.corda.core.flows.FlowLogic +import net.corda.core.flows.IllegalFlowLogicException import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl -import org.junit.Before import org.junit.Test import java.time.Duration @@ -31,67 +30,51 @@ class FlowLogicRefTest { override fun call() = Unit } - @Suppress("UNUSED_PARAMETER") // We will never use A or b - class NotWhiteListedKotlinFlowLogic(A: Int, b: String) : FlowLogic() { + class NonSchedulableFlow : FlowLogic() { override fun call() = Unit } - lateinit var factory: FlowLogicRefFactoryImpl - - @Before - fun setup() { - // We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them - factory = FlowLogicRefFactoryImpl(mapOf(Pair(KotlinFlowLogic::class.java.name, setOf(ParamType1::class.java.name, ParamType2::class.java.name)), - Pair(KotlinNoArgFlowLogic::class.java.name, setOf()))) + @Test + fun `create kotlin no arg`() { + FlowLogicRefFactoryImpl.createForRPC(KotlinNoArgFlowLogic::class.java) } @Test - fun testCreateKotlinNoArg() { - factory.create(KotlinNoArgFlowLogic::class.java) - } - - @Test - fun testCreateKotlin() { + fun `create kotlin`() { val args = mapOf(Pair("A", ParamType1(1)), Pair("b", ParamType2("Hello Jack"))) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreatePrimary() { - factory.create(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) - } - - @Test(expected = IllegalArgumentException::class) - fun testCreateNotWhiteListed() { - factory.create(NotWhiteListedKotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) + fun `create primary`() { + FlowLogicRefFactoryImpl.createForRPC(KotlinFlowLogic::class.java, ParamType1(1), ParamType2("Hello Jack")) } @Test - fun testCreateKotlinVoid() { - factory.createKotlin(KotlinFlowLogic::class.java, emptyMap()) + fun `create kotlin void`() { + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, emptyMap()) } @Test - fun testCreateKotlinNonPrimary() { + fun `create kotlin non primary`() { val args = mapOf(Pair("C", ParamType2("Hello Jack"))) - factory.createKotlin(KotlinFlowLogic::class.java, args) - } - - @Test(expected = IllegalArgumentException::class) - fun testCreateArgNotWhiteListed() { - val args = mapOf(Pair("illegal", 1.days)) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreateJavaPrimitiveNoRegistrationRequired() { + fun `create java primitive no registration required`() { val args = mapOf(Pair("primitive", "A string")) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) } @Test - fun testCreateKotlinPrimitiveNoRegistrationRequired() { + fun `create kotlin primitive no registration required`() { val args = mapOf(Pair("kotlinType", 3)) - factory.createKotlin(KotlinFlowLogic::class.java, args) + FlowLogicRefFactoryImpl.createKotlin(KotlinFlowLogic::class.java, args) + } + + @Test(expected = IllegalFlowLogicException::class) + fun `create for non-schedulable flow logic`() { + FlowLogicRefFactoryImpl.create(NonSchedulableFlow::class.java) } } diff --git a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt index eda373e2db..51c9f06c92 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/NodeSchedulerServiceTest.kt @@ -5,6 +5,7 @@ import net.corda.core.days import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRefFactory +import net.corda.core.identity.AbstractParty import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken @@ -44,10 +45,6 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val schedulerGatedExecutor = AffinityExecutor.Gate(true) - // We have to allow Java boxed primitives but Kotlin warns we shouldn't be using them - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - val factory = FlowLogicRefFactoryImpl(mapOf(Pair(TestFlowLogic::class.java.name, setOf(NodeSchedulerServiceTest::class.java.name, Integer::class.java.name)))) - lateinit var services: MockServiceHubInternal lateinit var scheduler: NodeSchedulerService @@ -82,12 +79,16 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { val kms = MockKeyManagementService(ALICE_KEY) val nullIdentity = X500Name("cn=None") - val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging(false, InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), AffinityExecutor.ServiceAffinityExecutor("test", 1), database) + val mockMessagingService = InMemoryMessagingNetwork(false).InMemoryMessaging( + false, + InMemoryMessagingNetwork.PeerHandle(0, nullIdentity), + AffinityExecutor.ServiceAffinityExecutor("test", 1), + database) services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } - scheduler = NodeSchedulerService(services, database, factory, schedulerGatedExecutor) + scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database) mockSMM.changes.subscribe { change -> @@ -113,7 +114,7 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { } class TestState(val flowLogicRef: FlowLogicRef, val instant: Instant) : LinearState, SchedulableState { - override val participants: List + override val participants: List get() = throw UnsupportedOperationException() override val linearId = UniqueIdentifier() @@ -269,12 +270,12 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { database.transaction { apply { val freshKey = services.keyManagementService.freshKey() - val state = TestState(factory.create(TestFlowLogic::class.java, increment), instant) - val usefulTX = TransactionType.General.Builder(null).apply { + val state = TestState(FlowLogicRefFactoryImpl.createForRPC(TestFlowLogic::class.java, increment), instant) + val builder = TransactionType.General.Builder(null).apply { addOutputState(state, DUMMY_NOTARY) - addCommand(Command(), freshKey.public) - signWith(freshKey) - }.toSignedTransaction() + addCommand(Command(), freshKey) + } + val usefulTX = services.signInitialTransaction(builder, freshKey) val txHash = usefulTX.id services.recordTransactions(usefulTX) diff --git a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt index fdbee20bdd..db951ed371 100644 --- a/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/events/ScheduledFlowTests.kt @@ -2,12 +2,13 @@ package net.corda.node.services.events import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.identity.Party import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogicRefFactory -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.flows.SchedulableFlow +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType import net.corda.core.utilities.DUMMY_NOTARY @@ -15,7 +16,6 @@ import net.corda.flows.FinalityFlow import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.StateMachineManager import net.corda.node.services.transactions.ValidatingNotaryService -import net.corda.node.utilities.AddOrRemove import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.junit.After @@ -47,10 +47,10 @@ class ScheduledFlowTests { } } - override val participants: List = listOf(source.owningKey, destination.owningKey) + override val participants: List = listOf(source, destination) override fun isRelevant(ourKeys: Set): Boolean { - return participants.any { it.containsAny(ourKeys) } + return participants.any { it.owningKey.containsAny(ourKeys) } } } @@ -62,12 +62,13 @@ class ScheduledFlowTests { val notary = serviceHub.networkMapCache.getAnyNotary() val builder = TransactionType.General.Builder(notary) - val tx = builder.withItems(scheduledState). - signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + builder.withItems(scheduledState) + val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(serviceHub.myInfo.legalIdentity))) } } + @SchedulableFlow class ScheduledFlow(val stateRef: StateRef) : FlowLogic() { @Suspendable override fun call() { @@ -81,20 +82,12 @@ class ScheduledFlowTests { val notary = state.state.notary val newStateOutput = scheduledState.copy(processed = true) val builder = TransactionType.General.Builder(notary) - val tx = builder.withItems(state, newStateOutput). - signWith(serviceHub.legalIdentityKey).toSignedTransaction(false) + builder.withItems(state, newStateOutput) + val tx = serviceHub.signInitialTransaction(builder) subFlow(FinalityFlow(tx, setOf(scheduledState.source, scheduledState.destination))) } } - object ScheduledFlowTestPlugin : CordaPluginRegistry() { - override val requiredFlows: Map> = mapOf( - InsertInitialStateFlow::class.java.name to setOf(Party::class.java.name), - ScheduledFlow::class.java.name to setOf(StateRef::class.java.name) - ) - } - - @Before fun setup() { net = MockNetwork(threadPerNode = true) @@ -103,8 +96,6 @@ class ScheduledFlowTests { advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) nodeA = net.createNode(notaryNode.info.address, start = false) nodeB = net.createNode(notaryNode.info.address, start = false) - nodeA.testPluginRegistries.add(ScheduledFlowTestPlugin) - nodeB.testPluginRegistries.add(ScheduledFlowTestPlugin) net.startNodes() } @@ -138,7 +129,7 @@ class ScheduledFlowTests { } @Test - fun `Run a whole batch of scheduled flows`() { + fun `run a whole batch of scheduled flows`() { val N = 100 for (i in 0..N - 1) { nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) diff --git a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt index 0b24ef80d7..c47e9b3b7c 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/AbstractNetworkMapServiceTest.kt @@ -216,7 +216,7 @@ abstract class AbstractNetworkMapServiceTest } val expires = Instant.now() + NetworkMapService.DEFAULT_EXPIRATION_PERIOD val nodeRegistration = NodeRegistration(info, distinctSerial, addOrRemove, expires) - val request = RegistrationRequest(nodeRegistration.toWire(services.legalIdentityKey.private), info.address) + val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), info.address) val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.info.address) network.runNetwork() return response diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt index 90c88bb727..f6057b11e1 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryIdentityServiceTests.kt @@ -1,23 +1,25 @@ package net.corda.node.services.network -import net.corda.core.identity.Party import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.generateKeyPair -import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.Party +import net.corda.core.node.services.IdentityService import net.corda.core.utilities.ALICE import net.corda.core.utilities.BOB +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.testing.ALICE_PUBKEY import net.corda.testing.BOB_PUBKEY import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import kotlin.test.assertEquals +import kotlin.test.assertFailsWith import kotlin.test.assertNull /** * Tests for the in memory identity service. */ class InMemoryIdentityServiceTests { - @Test fun `get all identities`() { val service = InMemoryIdentityService() @@ -58,4 +60,54 @@ class InMemoryIdentityServiceTests { identities.forEach { service.registerIdentity(it) } identities.forEach { assertEquals(it, service.partyFromX500Name(it.name)) } } + + /** + * Generate a certificate path from a root CA, down to a transaction key, store and verify the association. + */ + @Test + fun `assert unknown anonymous key is unrecognised`() { + val rootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) + val txCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, rootCertAndKey) + val service = InMemoryIdentityService() + val rootKey = rootCertAndKey.keyPair + // TODO: Generate certificate with an EdDSA key rather than ECDSA + val identity = Party(rootCertAndKey) + val txIdentity = AnonymousParty(txCertAndKey.keyPair.public) + + assertFailsWith { + service.assertOwnership(identity, txIdentity) + } + } + + /** + * Generate a pair of certificate paths from a root CA, down to a transaction key, store and verify the associations. + * Also checks that incorrect associations are rejected. + */ + @Test + fun `assert ownership`() { + val aliceRootCertAndKey = X509Utilities.createSelfSignedCACert(ALICE.name) + val aliceTxCertAndKey = X509Utilities.createIntermediateCert(ALICE.name, aliceRootCertAndKey) + val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCertAndKey, aliceTxCertAndKey.certificate, false).certPath + val bobRootCertAndKey = X509Utilities.createSelfSignedCACert(BOB.name) + val bobTxCertAndKey = X509Utilities.createIntermediateCert(BOB.name, bobRootCertAndKey) + val bobCertPath = X509Utilities.createCertificatePath(bobRootCertAndKey, bobTxCertAndKey.certificate, false).certPath + val service = InMemoryIdentityService() + val alice = Party(aliceRootCertAndKey) + val anonymousAlice = AnonymousParty(aliceTxCertAndKey.keyPair.public) + val bob = Party(bobRootCertAndKey) + val anonymousBob = AnonymousParty(bobTxCertAndKey.keyPair.public) + + service.registerPath(aliceRootCertAndKey.certificate, anonymousAlice, aliceCertPath) + service.registerPath(bobRootCertAndKey.certificate, anonymousBob, bobCertPath) + + // Verify that paths are verified + service.assertOwnership(alice, anonymousAlice) + service.assertOwnership(bob, anonymousBob) + assertFailsWith { + service.assertOwnership(alice, anonymousBob) + } + assertFailsWith { + service.assertOwnership(bob, anonymousAlice) + } + } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt index 9064db174e..8d969ce1c4 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DataVendingServiceTests.kt @@ -37,7 +37,7 @@ class DataVendingServiceTests { @Test fun `notify of transaction`() { val (vaultServiceNode, registerNode) = network.createTwoNodes() - val beneficiary = vaultServiceNode.info.legalIdentity.owningKey + val beneficiary = vaultServiceNode.info.legalIdentity val deposit = registerNode.info.legalIdentity.ref(1) network.runNetwork() @@ -46,9 +46,7 @@ class DataVendingServiceTests { Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY) // Complete the cash transaction, and then manually relay it - val registerKey = registerNode.services.legalIdentityKey - ptx.signWith(registerKey) - val tx = ptx.toSignedTransaction() + val tx = registerNode.services.signInitialTransaction(ptx) vaultServiceNode.database.transaction { assertThat(vaultServiceNode.services.vaultService.unconsumedStates()).isEmpty() @@ -67,7 +65,7 @@ class DataVendingServiceTests { @Test fun `notify failure`() { val (vaultServiceNode, registerNode) = network.createTwoNodes() - val beneficiary = vaultServiceNode.info.legalIdentity.owningKey + val beneficiary = vaultServiceNode.info.legalIdentity val deposit = MEGA_CORP.ref(1) network.runNetwork() @@ -76,9 +74,7 @@ class DataVendingServiceTests { Cash().generateIssue(ptx, Amount(100, Issued(deposit, USD)), beneficiary, DUMMY_NOTARY) // The transaction tries issuing MEGA_CORP cash, but we aren't the issuer, so it's invalid - val registerKey = registerNode.services.legalIdentityKey - ptx.signWith(registerKey) - val tx = ptx.toSignedTransaction(false) + val tx = registerNode.services.signInitialTransaction(ptx) vaultServiceNode.database.transaction { assertThat(vaultServiceNode.services.vaultService.unconsumedStates()).isEmpty() diff --git a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt index 75c468dd4f..08dffaa371 100644 --- a/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/schema/HibernateObserverTests.kt @@ -6,6 +6,7 @@ import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionState import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.SecureHash +import net.corda.core.identity.AbstractParty import net.corda.core.node.services.Vault import net.corda.core.schemas.MappedSchema import net.corda.core.schemas.PersistentState @@ -54,7 +55,7 @@ class HibernateObserverTests { @Table(name = "Parents") class Parent : PersistentState() { @OneToMany(fetch = FetchType.LAZY) - @JoinColumns(JoinColumn(name = "transaction_id"), JoinColumn(name = "output_index")) + @JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index")) @OrderColumn @Cascade(CascadeType.PERSIST) var children: MutableSet = mutableSetOf() @@ -70,7 +71,7 @@ class HibernateObserverTests { var childId: Int? = null @ManyToOne(fetch = FetchType.LAZY) - @JoinColumns(JoinColumn(name = "transaction_id"), JoinColumn(name = "output_index")) + @JoinColumns(JoinColumn(name = "transaction_id", referencedColumnName = "transaction_id"), JoinColumn(name = "output_index", referencedColumnName = "output_index")) var parent: Parent? = null } @@ -86,7 +87,7 @@ class HibernateObserverTests { override val contract: Contract get() = throw UnsupportedOperationException() - override val participants: List + override val participants: List get() = throw UnsupportedOperationException() } diff --git a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt index 1cebf39987..7eece7a705 100644 --- a/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/statemachine/FlowFrameworkTests.kt @@ -560,8 +560,7 @@ class FlowFrameworkTests { fun `wait for transaction`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) ptx.addOutputState(DummyState()) - ptx.signWith(node1.services.legalIdentityKey) - val stx = ptx.toSignedTransaction() + val stx = node1.services.signInitialTransaction(ptx) val committerFiber = node1 .initiateSingleShotFlow(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) } @@ -575,8 +574,7 @@ class FlowFrameworkTests { fun `committer throws exception before calling the finality flow`() { val ptx = TransactionBuilder(notary = notary1.info.notaryIdentity) ptx.addOutputState(DummyState()) - ptx.signWith(node1.services.legalIdentityKey) - val stx = ptx.toSignedTransaction() + val stx = node1.services.signInitialTransaction(ptx) node1.registerServiceFlow(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt index ee01595eb2..408cd5a545 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/NotaryServiceTests.kt @@ -6,7 +6,6 @@ import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.contracts.TransactionType import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.keys import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds @@ -21,7 +20,6 @@ import net.corda.testing.node.MockNetwork import org.assertj.core.api.Assertions.assertThat import org.junit.Before import org.junit.Test -import java.security.KeyPair import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -31,7 +29,6 @@ class NotaryServiceTests { lateinit var net: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode - lateinit var clientKeyPair: KeyPair @Before fun setup() { net = MockNetwork() @@ -39,7 +36,6 @@ class NotaryServiceTests { legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) clientNode = net.createNode(networkMapAddress = notaryNode.info.address) - clientKeyPair = clientNode.keyManagement.toKeyPair(clientNode.info.legalIdentity.owningKey.keys.single()) net.runNetwork() // Clear network map registration messages } @@ -48,8 +44,7 @@ class NotaryServiceTests { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) tx.setTime(Instant.now(), 30.seconds) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -61,8 +56,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -75,8 +69,7 @@ class NotaryServiceTests { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) tx.setTime(Instant.now().plusSeconds(3600), 30.seconds) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val future = runNotaryClient(stx) @@ -89,8 +82,7 @@ class NotaryServiceTests { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val firstAttempt = NotaryFlow.Client(stx) @@ -107,14 +99,12 @@ class NotaryServiceTests { val inputState = issueState(clientNode) val stx = run { val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val stx2 = run { val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) tx.addInputState(issueState(clientNode)) - tx.signWith(clientKeyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val firstSpend = NotaryFlow.Client(stx) @@ -139,11 +129,8 @@ class NotaryServiceTests { fun issueState(node: AbstractNode): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) - val nodeKey = node.services.legalIdentityKey - tx.signWith(nodeKey) - val notaryKeyPair = notaryNode.services.notaryIdentityKey - tx.signWith(notaryKeyPair) - val stx = tx.toSignedTransaction() + val signedByNode = node.services.signInitialTransaction(tx) + val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) node.services.recordTransactions(listOf(stx)) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt index 2ffffa4402..dfe2cc29fa 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/ValidatingNotaryServiceTests.kt @@ -3,7 +3,6 @@ package net.corda.node.services.transactions import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature -import net.corda.core.crypto.keys import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.transactions.SignedTransaction @@ -42,9 +41,7 @@ class ValidatingNotaryServiceTests { val stx = run { val inputState = issueInvalidState(clientNode, notaryNode.info.notaryIdentity) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - val keyPair = clientNode.services.keyManagementService.toKeyPair(clientNode.info.legalIdentity.owningKey.keys.single()) - tx.signWith(keyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val future = runClient(stx) @@ -60,9 +57,7 @@ class ValidatingNotaryServiceTests { val command = Command(DummyContract.Commands.Move(), expectedMissingKey) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState, command) - val keyPair = clientNode.services.keyManagementService.toKeyPair(clientNode.info.legalIdentity.owningKey.keys.single()) - tx.signWith(keyPair) - tx.toSignedTransaction(false) + clientNode.services.signInitialTransaction(tx) } val ex = assertFailsWith(NotaryException::class) { @@ -85,11 +80,8 @@ class ValidatingNotaryServiceTests { fun issueState(node: AbstractNode): StateAndRef<*> { val tx = DummyContract.generateInitial(Random().nextInt(), notaryNode.info.notaryIdentity, node.info.legalIdentity.ref(0)) - val nodeKey = node.services.legalIdentityKey - tx.signWith(nodeKey) - val notaryKeyPair = notaryNode.services.notaryIdentityKey - tx.signWith(notaryKeyPair) - val stx = tx.toSignedTransaction() + val signedByNode = node.services.signInitialTransaction(tx) + val stx = notaryNode.services.addSignature(signedByNode, notaryNode.services.notaryIdentityKey) node.services.recordTransactions(listOf(stx)) return StateAndRef(tx.outputStates().first(), StateRef(stx.id, 0)) } diff --git a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt index 8190cd02cf..a4164e3008 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/NodeVaultServiceTest.kt @@ -4,6 +4,7 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.core.contracts.* +import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.StatesNotAvailableException import net.corda.core.node.services.TxWritableStorageService import net.corda.core.node.services.VaultService @@ -405,7 +406,7 @@ class NodeVaultServiceTest { // Issue a txn to Send us some Money val usefulTX = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() @@ -418,7 +419,7 @@ class NodeVaultServiceTest { // Issue more Money (GBP) val anotherTX = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 200.POUNDS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + Cash().generateIssue(this, 200.POUNDS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt index 7ee8bcf34d..db8374cf34 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultQueryTests.kt @@ -206,9 +206,9 @@ class VaultQueryTests { fun `unconsumed states by participants`() { database.transaction { - services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST"), participants = listOf(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY)) - services.fillWithSomeTestDeals(listOf("456"), 3, participants = listOf(MEGA_CORP_PUBKEY, BIG_CORP_PUBKEY)) - services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP_PUBKEY, MINI_CORP_PUBKEY)) + services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST"), participants = listOf(MEGA_CORP, MINI_CORP)) + services.fillWithSomeTestDeals(listOf("456"), 3, participants = listOf(MEGA_CORP, BIG_CORP)) + services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP, MINI_CORP)) // DOCSTART VaultQueryExample5 val criteria = VaultQueryCriteria(participantIdentities = listOf(MEGA_CORP.name, MINI_CORP.name)) diff --git a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt index fe87a6aee9..9e8fd69af9 100644 --- a/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/vault/VaultWithCashTest.kt @@ -4,17 +4,17 @@ import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.* import net.corda.core.contracts.* +import net.corda.core.identity.AnonymousParty import net.corda.core.node.services.VaultService import net.corda.core.node.services.consumedStates import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.BOB_KEY +import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.LogHelper import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction -import net.corda.testing.BOB_PUBKEY import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.node.MockServices @@ -79,7 +79,7 @@ class VaultWithCashTest { val state = w[0].state.data assertEquals(30.45.DOLLARS `issued by` DUMMY_CASH_ISSUER, state.amount) - assertEquals(services.key.public, state.owner) + assertEquals(services.key.public, state.owner.owningKey) assertEquals(34.70.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[2].state.data).amount) assertEquals(34.85.DOLLARS `issued by` DUMMY_CASH_ISSUER, (w[1].state.data).amount) @@ -92,7 +92,7 @@ class VaultWithCashTest { // A tx that sends us money. val freshKey = services.keyManagementService.freshKey() val usefulTX = TransactionType.General.Builder(null).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), freshKey.public, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), AnonymousParty(freshKey), DUMMY_NOTARY) signWith(MEGA_CORP_KEY) }.toSignedTransaction() @@ -100,17 +100,17 @@ class VaultWithCashTest { services.recordTransactions(usefulTX) // A tx that spends our money. - val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY) - signWith(freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { + vault.generateSpend(this, 80.DOLLARS, BOB) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) assertEquals(100.DOLLARS, vault.cashBalances[USD]) // A tx that doesn't send us anything. val irrelevantTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB_KEY.public, DUMMY_NOTARY) + Cash().generateIssue(this, 100.DOLLARS `issued by` MEGA_CORP.ref(1), BOB, DUMMY_NOTARY) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -134,7 +134,7 @@ class VaultWithCashTest { services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 10, 10, Random(0L), issuedBy = MEGA_CORP.ref(1), issuerKey = MEGA_CORP_KEY, - ownedBy = freshKey.public) + ownedBy = AnonymousParty(freshKey)) println("Cash balance: ${vault.cashBalances[USD]}") assertThat(vault.unconsumedStates()).hasSize(10) @@ -147,12 +147,12 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn1 = + val txn1Builder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 60.DOLLARS, BOB_PUBKEY) - signWith(freshKey) + vault.generateSpend(this, 60.DOLLARS, BOB) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val txn1 = services.signInitialTransaction(txn1Builder, freshKey) println("txn1: ${txn1.id} spent ${((txn1.tx.outputs[0].data) as Cash.State).amount}") println("""txn1 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -179,12 +179,12 @@ class VaultWithCashTest { backgroundExecutor.submit { database.transaction { try { - val txn2 = + val txn2Builder = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY) - signWith(freshKey) + vault.generateSpend(this, 80.DOLLARS, BOB) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val txn2 = services.signInitialTransaction(txn2Builder, freshKey) println("txn2: ${txn2.id} spent ${((txn2.tx.outputs[0].data) as Cash.State).amount}") println("""txn2 states: UNCONSUMED: ${vault.unconsumedStates().count()} : ${vault.unconsumedStates()}, @@ -219,15 +219,16 @@ class VaultWithCashTest { fun `branching LinearStates fails to verify`() { database.transaction { val freshKey = services.keyManagementService.freshKey() + val freshIdentity = AnonymousParty(freshKey) val linearId = UniqueIdentifier() // Issue a linear state - val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) - signWith(freshKey) + val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val dummyIssue = services.signInitialTransaction(dummyIssueBuilder) assertThatThrownBy { dummyIssue.toLedgerTransaction(services).verify() @@ -239,15 +240,16 @@ class VaultWithCashTest { fun `sequencing LinearStates works`() { database.transaction { val freshKey = services.keyManagementService.freshKey() + val freshIdentity = AnonymousParty(freshKey) val linearId = UniqueIdentifier() // Issue a linear state - val dummyIssue = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) - signWith(freshKey) + val dummyIssueBuilder = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val dummyIssue = services.signInitialTransaction(dummyIssueBuilder, services.legalIdentityKey) dummyIssue.toLedgerTransaction(services).verify() @@ -256,7 +258,7 @@ class VaultWithCashTest { // Move the same state val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(linearId = linearId, participants = listOf(freshIdentity))) addInputState(dummyIssue.tx.outRef(0)) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -273,7 +275,7 @@ class VaultWithCashTest { val freshKey = services.keyManagementService.freshKey() database.transaction { - services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = freshKey.public) + services.fillWithSomeTestCash(100.DOLLARS, DUMMY_NOTARY, 3, 3, Random(0L), ownedBy = AnonymousParty(freshKey)) services.fillWithSomeTestCash(100.SWISS_FRANCS, DUMMY_NOTARY, 2, 2, Random(0L)) services.fillWithSomeTestCash(100.POUNDS, DUMMY_NOTARY, 1, 1, Random(0L)) val cash = vault.unconsumedStates() @@ -286,11 +288,11 @@ class VaultWithCashTest { database.transaction { // A tx that spends our money. - val spendTX = TransactionType.General.Builder(DUMMY_NOTARY).apply { - vault.generateSpend(this, 80.DOLLARS, BOB_PUBKEY) - signWith(freshKey) + val spendTXBuilder = TransactionType.General.Builder(DUMMY_NOTARY).apply { + vault.generateSpend(this, 80.DOLLARS, BOB) signWith(DUMMY_NOTARY_KEY) - }.toSignedTransaction() + } + val spendTX = services.signInitialTransaction(spendTXBuilder, freshKey) services.recordTransactions(spendTX) val consumedStates = vault.consumedStates() @@ -305,6 +307,7 @@ class VaultWithCashTest { fun `consuming multiple contract state types in same transaction`() { val freshKey = services.keyManagementService.freshKey() + val freshIdentity = AnonymousParty(freshKey) database.transaction { services.fillWithSomeTestDeals(listOf("123", "456", "789")) @@ -317,8 +320,8 @@ class VaultWithCashTest { // Create a txn consuming different contract types val dummyMove = TransactionType.General.Builder(notary = DUMMY_NOTARY).apply { - addOutputState(DummyLinearContract.State(participants = listOf(freshKey.public))) - addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshKey.public))) + addOutputState(DummyLinearContract.State(participants = listOf(freshIdentity))) + addOutputState(DummyDealContract.State(ref = "999", participants = listOf(freshIdentity))) addInputState(linearStates.first()) addInputState(deals.first()) signWith(DUMMY_NOTARY_KEY) diff --git a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt index 6bcbb5f65c..0b9ca23c58 100644 --- a/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt +++ b/samples/attachment-demo/src/integration-test/kotlin/net/corda/attachmentdemo/AttachmentDemoTest.kt @@ -7,6 +7,7 @@ import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.driver.driver +import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import org.junit.Test @@ -17,11 +18,11 @@ class AttachmentDemoTest { @Test fun `attachment demo using a 10MB zip file`() { val numOfExpectedBytes = 10_000_000 driver(dsl = { - val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) + val demoUser = listOf(User("demo", "demo", setOf(startFlowPermission()))) val (nodeA, nodeB) = Futures.allAsList( startNode(DUMMY_BANK_A.name, rpcUsers = demoUser), startNode(DUMMY_BANK_B.name, rpcUsers = demoUser), - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.Companion.type))) + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) ).getOrThrow() val senderThread = CompletableFuture.supplyAsync { diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt index ee24480e6d..745d83e4cf 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/AttachmentDemo.kt @@ -1,5 +1,6 @@ package net.corda.attachmentdemo +import co.paralleluniverse.fibers.Suspendable import com.google.common.net.HostAndPort import joptsimple.OptionParser import net.corda.client.rpc.CordaRPCClient @@ -9,14 +10,15 @@ import net.corda.core.contracts.TransactionForContract import net.corda.core.contracts.TransactionType import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startTrackedFlow import net.corda.core.sizedInputStreamAndHash -import net.corda.core.utilities.DUMMY_BANK_B -import net.corda.core.utilities.DUMMY_NOTARY -import net.corda.core.utilities.DUMMY_NOTARY_KEY -import net.corda.core.utilities.Emoji +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.* import net.corda.flows.FinalityFlow import net.corda.node.driver.poll import java.io.InputStream @@ -83,26 +85,40 @@ fun sender(rpc: CordaRPCOps, inputStream: InputStream, hash: SecureHash.SHA256) val id = rpc.uploadAttachment(it) require(hash == id) { "Id was '$id' instead of '$hash'" } } + require(rpc.attachmentExists(hash)) } - // Create a trivial transaction with an output that describes the attachment, and the attachment itself - val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY) - require(rpc.attachmentExists(hash)) - ptx.addOutputState(AttachmentContract.State(hash)) - ptx.addAttachment(hash) - - // Sign with the notary key - ptx.signWith(DUMMY_NOTARY_KEY) - - // Send the transaction to the other recipient - val stx = ptx.toSignedTransaction() - println("Sending ${stx.id}") - val flowHandle = rpc.startTrackedFlow(::FinalityFlow, stx, setOf(otherSide)) + val flowHandle = rpc.startTrackedFlow(::AttachmentDemoFlow, otherSide, hash) flowHandle.progress.subscribe(::println) - flowHandle.returnValue.getOrThrow() + val stx = flowHandle.returnValue.getOrThrow() println("Sent ${stx.id}") } +@StartableByRPC +class AttachmentDemoFlow(val otherSide: Party, val hash: SecureHash.SHA256) : FlowLogic() { + + object SIGNING : ProgressTracker.Step("Signing transaction") + + override val progressTracker: ProgressTracker = ProgressTracker(SIGNING) + + @Suspendable + override fun call(): SignedTransaction { + // Create a trivial transaction with an output that describes the attachment, and the attachment itself + val ptx = TransactionType.General.Builder(notary = DUMMY_NOTARY) + ptx.addOutputState(AttachmentContract.State(hash)) + ptx.addAttachment(hash) + + progressTracker.currentStep = SIGNING + // Sign with the notary key + ptx.signWith(DUMMY_NOTARY_KEY) + + // Send the transaction to the other recipient + val stx = ptx.toSignedTransaction() + + return subFlow(FinalityFlow(stx, setOf(otherSide))).single() + } +} + fun recipient(rpc: CordaRPCOps) { println("Waiting to receive transaction ...") val stx = rpc.verifiedTransactions().second.toBlocking().first() @@ -164,6 +180,6 @@ class AttachmentContract : Contract { data class State(val hash: SecureHash.SHA256) : ContractState { override val contract: Contract = AttachmentContract() - override val participants: List = emptyList() + override val participants: List = emptyList() } } diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt index ec1c836120..9e7485eda3 100644 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt +++ b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/Main.kt @@ -8,7 +8,6 @@ import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User -import java.nio.file.Paths /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) @@ -16,8 +15,8 @@ import java.nio.file.Paths */ fun main(args: Array) { val demoUser = listOf(User("demo", "demo", setOf("StartFlow.net.corda.flows.FinalityFlow"))) - driver(isDebug = true, driverDirectory = Paths.get("build") / "attachment-demo-nodes") { - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.Companion.type))) + driver(isDebug = true, driverDirectory = "build" / "attachment-demo-nodes") { + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) startNode(DUMMY_BANK_A.name, rpcUsers = demoUser) startNode(DUMMY_BANK_B.name, rpcUsers = demoUser) waitForAllNodesToFinish() diff --git a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt b/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt deleted file mode 100644 index 33c80473ed..0000000000 --- a/samples/attachment-demo/src/main/kotlin/net/corda/attachmentdemo/plugin/AttachmentDemoPlugin.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.attachmentdemo.plugin - -import net.corda.core.node.CordaPluginRegistry -import net.corda.core.transactions.SignedTransaction -import net.corda.flows.FinalityFlow - -class AttachmentDemoPlugin : CordaPluginRegistry() { - // A list of Flows that are required for this cordapp - override val requiredFlows: Map> = mapOf( - FinalityFlow::class.java.name to setOf(SignedTransaction::class.java.name, setOf(Unit).javaClass.name, setOf(Unit).javaClass.name) - ) -} diff --git a/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index 2c117fcac4..0000000000 --- a/samples/attachment-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,2 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.attachmentdemo.plugin.AttachmentDemoPlugin diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 981fec514c..39ea0da9ec 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -52,7 +52,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = [] } node { - name "CN=BankOfCorda,O=R3,OU=corda,L=New York,C=US" + name "CN=BankOfCorda,O=R3,OU=Corda QA Department,L=New York,C=US" nearestCity "New York" advertisedServices = ["corda.issuer.USD"] p2pPort 10005 diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt index e0d12385c3..8fd4ce47bb 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/plugin/BankOfCordaPlugin.kt @@ -1,17 +1,13 @@ package net.corda.bank.plugin import net.corda.bank.api.BankOfCordaWebApi -import net.corda.core.contracts.Amount import net.corda.core.identity.Party import net.corda.core.node.CordaPluginRegistry -import net.corda.core.serialization.OpaqueBytes import net.corda.flows.IssuerFlow import java.util.function.Function class BankOfCordaPlugin : CordaPluginRegistry() { // A list of classes that expose web APIs. override val webApis = listOf(Function(::BankOfCordaWebApi)) - // A list of flow that are required for this cordapp - override val requiredFlows: Map> = mapOf(IssuerFlow.IssuanceRequester::class.java.name to setOf(Amount::class.java.name, Party::class.java.name, OpaqueBytes::class.java.name, Party::class.java.name)) override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) } diff --git a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt index d0fabb3c87..01b0a588b0 100644 --- a/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt +++ b/samples/irs-demo/src/integration-test/kotlin/net/corda/irs/IRSDemoTest.kt @@ -70,7 +70,7 @@ class IRSDemoTest : IntegrationTestCategory { val vaultUpdates = proxy.vaultAndUpdates().second val fixingDates = vaultUpdates.map { update -> - val irsStates = update.produced.map { it.state.data }.filterIsInstance>() + val irsStates = update.produced.map { it.state.data }.filterIsInstance() irsStates.mapNotNull { it.calculation.nextFixingDate() }.max() }.cache().toBlocking() @@ -83,14 +83,14 @@ class IRSDemoTest : IntegrationTestCategory { } private fun runTrade(nodeAddr: HostAndPort) { - val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("example-irs-trade.json"), Charsets.UTF_8.name()) + val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name()) val tradeFile = fileContents.replace("tradeXXX", "trade1") val url = URL("http://$nodeAddr/api/irs/deals") assertThat(postJson(url, tradeFile)).isTrue() } private fun runUploadRates(host: HostAndPort) { - val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("example.rates.txt"), Charsets.UTF_8.name()) + val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()) val url = URL("http://$host/upload/interest-rates") assertThat(uploadFile(url, fileContents)).isTrue() } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/InterestRateSwapAPI.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/InterestRateSwapAPI.kt index 4c6bc8b864..8f09afaebf 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/InterestRateSwapAPI.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/InterestRateSwapAPI.kt @@ -40,22 +40,22 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) { private val logger = loggerFor() - private fun generateDealLink(deal: InterestRateSwap.State<*>) = "/api/irs/deals/" + deal.common.tradeID + private fun generateDealLink(deal: InterestRateSwap.State) = "/api/irs/deals/" + deal.common.tradeID - private fun getDealByRef(ref: String): InterestRateSwap.State<*>? { + private fun getDealByRef(ref: String): InterestRateSwap.State? { val (vault, vaultUpdates) = rpc.vaultAndUpdates() vaultUpdates.notUsed() - val states = vault.filterStatesOfType>().filter { it.state.data.ref == ref } + val states = vault.filterStatesOfType().filter { it.state.data.ref == ref } return if (states.isEmpty()) null else { val deals = states.map { it.state.data } return if (deals.isEmpty()) null else deals[0] } } - private fun getAllDeals(): Array> { + private fun getAllDeals(): Array { val (vault, vaultUpdates) = rpc.vaultAndUpdates() vaultUpdates.notUsed() - val states = vault.filterStatesOfType>() + val states = vault.filterStatesOfType() val swaps = states.map { it.state.data }.toTypedArray() return swaps } @@ -63,14 +63,14 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) { @GET @Path("deals") @Produces(MediaType.APPLICATION_JSON) - fun fetchDeals(): Array> = getAllDeals() + fun fetchDeals(): Array = getAllDeals() @POST @Path("deals") @Consumes(MediaType.APPLICATION_JSON) - fun storeDeal(newDeal: InterestRateSwap.State): Response { + fun storeDeal(newDeal: InterestRateSwap.State): Response { return try { - rpc.startFlow(AutoOfferFlow::Requester, newDeal.toAnonymous()).returnValue.getOrThrow() + rpc.startFlow(AutoOfferFlow::Requester, newDeal).returnValue.getOrThrow() Response.created(URI.create(generateDealLink(newDeal))).build() } catch (ex: Throwable) { logger.info("Exception when creating deal: $ex") diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt index 16620d53a4..2a179ec611 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt @@ -13,7 +13,7 @@ class IRSDemoClientApi(private val hostAndPort: HostAndPort) { private val api = HttpApi.fromHostAndPort(hostAndPort, apiRoot) fun runTrade(tradeId: String): Boolean { - val fileContents = IOUtils.toString(javaClass.classLoader.getResourceAsStream("example-irs-trade.json"), Charsets.UTF_8.name()) + val fileContents = IOUtils.toString(javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name()) val tradeFile = fileContents.replace("tradeXXX", tradeId) return api.postJson("deals", tradeFile) } @@ -24,7 +24,7 @@ class IRSDemoClientApi(private val hostAndPort: HostAndPort) { // TODO: Add uploading of files to the HTTP API fun runUploadRates() { - val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("example.rates.txt"), Charsets.UTF_8.name()) + val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()) val url = URL("http://$hostAndPort/upload/interest-rates") check(uploadFile(url, fileContents)) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt index 7ba75b4a64..b485f406a1 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt @@ -5,21 +5,21 @@ import net.corda.core.RetryableException import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.MerkleTreeException -import net.corda.core.identity.Party import net.corda.core.crypto.keys import net.corda.core.crypto.sign import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party import net.corda.core.math.CubicSplineInterpolator import net.corda.core.math.Interpolator import net.corda.core.math.InterpolatorFactory import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.PluginServiceHub +import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceType import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap -import net.corda.irs.flows.FixingFlow import net.corda.irs.flows.RatesFixFlow import net.corda.node.services.api.AcceptsFileUpload import net.corda.node.utilities.AbstractJDBCHashSet @@ -32,12 +32,16 @@ import java.io.InputStream import java.math.BigDecimal import java.security.KeyPair import java.time.Clock +import java.security.PublicKey import java.time.Duration import java.time.Instant import java.time.LocalDate import java.util.* import java.util.function.Function import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set /** * An interest rates service is an oracle that signs transactions which contain embedded assertions about an interest @@ -55,7 +59,6 @@ object NodeInterestRates { * Register the flow that is used with the Fixing integration tests. */ class Plugin : CordaPluginRegistry() { - override val requiredFlows = mapOf(Pair(FixingFlow.FixingRoleDecider::class.java.name, setOf(Duration::class.java.name, StateRef::class.java.name))) override val servicePlugins = listOf(Function(::Service)) } @@ -67,8 +70,8 @@ object NodeInterestRates { val oracle: Oracle by lazy { val myNodeInfo = services.myInfo val myIdentity = myNodeInfo.serviceIdentities(type).first() - val mySigningKey = services.keyManagementService.toKeyPair(myIdentity.owningKey.keys) - Oracle(myIdentity, mySigningKey, services.clock) + val mySigningKey = myIdentity.owningKey.keys.first { services.keyManagementService.keys.contains(it) } + Oracle(myIdentity, mySigningKey, services) } init { @@ -128,7 +131,7 @@ object NodeInterestRates { * The oracle will try to interpolate the missing value of a tenor for the given fix name and date. */ @ThreadSafe - class Oracle(val identity: Party, private val signingKey: KeyPair, val clock: Clock) { + class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) { private object Table : JDBCHashedTable("demo_interest_rate_fixes") { val name = varchar("index_name", length = 255) val forDay = localDate("for_day") @@ -167,7 +170,7 @@ object NodeInterestRates { // Make this the last bit of initialisation logic so fully constructed when entered into instances map init { - require(signingKey.public in identity.owningKey.keys) + require(signingKey in identity.owningKey.keys) } /** @@ -178,7 +181,7 @@ object NodeInterestRates { @Suspendable fun query(queries: List, deadline: Instant): List { require(queries.isNotEmpty()) - return mutex.readWithDeadline(clock, deadline) { + return mutex.readWithDeadline(services.clock, deadline) { val answers: List = queries.map { container[it] } val firstNull = answers.indexOf(null) if (firstNull != -1) { @@ -224,7 +227,8 @@ object NodeInterestRates { // Note that we will happily sign an invalid transaction, as we are only being presented with a filtered // version so we can't resolve or check it ourselves. However, that doesn't matter much, as if we sign // an invalid transaction the signature is worthless. - return signingKey.sign(ftx.rootHash.bytes, identity) + val signature = services.keyManagementService.sign(ftx.rootHash.bytes, signingKey) + return DigitalSignature.LegallyIdentifiable(identity, signature.bytes) } // DOCEND 1 } @@ -245,10 +249,9 @@ object NodeInterestRates { private fun buildContainer(fixes: Set): Map, InterpolatingRateMap> { val tempContainer = HashMap, HashMap>() - for (fix in fixes) { - val fixOf = fix.of + for ((fixOf, value) in fixes) { val rates = tempContainer.getOrPut(fixOf.name to fixOf.forDay) { HashMap() } - rates[fixOf.ofTenor] = fix.value + rates[fixOf.ofTenor] = value } // TODO: the calendar data needs to be specified for every fix type in the input string diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt index 2379573e8a..4c21148852 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRS.kt @@ -2,7 +2,8 @@ package net.corda.irs.contract import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.containsAny import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.identity.AbstractParty import net.corda.core.identity.AnonymousParty @@ -307,8 +308,8 @@ class InterestRateSwap : Contract { } @CordaSerializable - open class FixedLeg