diff --git a/.gitignore b/.gitignore index 03f02c5ed0..b2f09d9180 100644 --- a/.gitignore +++ b/.gitignore @@ -84,7 +84,7 @@ crashlytics-build.properties docs/virtualenv/ # bft-smart -config/currentView +**/config/currentView # vim *.swp diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 61d88634d8..4647555bea 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -38,6 +38,8 @@ + + @@ -62,6 +64,7 @@ + @@ -86,6 +89,9 @@ + + + diff --git a/README.md b/README.md index e592d86a21..d490c71ae2 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png) + + # Corda Corda is a decentralised database system in which nodes trust each other as little as possible. @@ -12,8 +14,6 @@ Corda is a decentralised database system in which nodes trust each other as litt * "Notary" infrastructure to validate uniqueness of transactions * Written as a platform for distributed apps called CorDapps * Written in [Kotlin](https://kotlinlang.org), targeting the JVM - -Read our full and planned feature list [here](https://docs.corda.net/inthebox.html). ## Getting started @@ -29,7 +29,7 @@ After the above, watching the following webinars will give you a great introduct ### Webinar 1 – [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1) -Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session. +Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3cev.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session. ### Webinar 2 – [Corda Developers’ Tutorial](https://vimeo.com/192797322/aab499b152) diff --git a/build.gradle b/build.gradle index 54bc126c8b..c975db734a 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { file("$projectDir/constants.properties").withInputStream { constants.load(it) } // Our version: bump this on release. - ext.corda_release_version = "0.12-SNAPSHOT" + ext.corda_release_version = "0.13-SNAPSHOT" // Increment this on any release that changes public APIs anywhere in the Corda platform // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal ext.corda_platform_version = 1 @@ -34,7 +34,7 @@ buildscript { ext.guava_version = constants.getProperty("guavaVersion") ext.quickcheck_version = '0.7' ext.okhttp_version = '3.5.0' - ext.netty_version = '4.1.5.Final' + ext.netty_version = '4.1.9.Final' ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion") ext.fileupload_version = '1.3.2' ext.junit_version = '4.12' @@ -45,7 +45,7 @@ buildscript { ext.h2_version = '1.4.194' ext.rxjava_version = '1.2.4' ext.requery_version = '1.2.1' - ext.dokka_version = '0.9.13' + ext.dokka_version = '0.9.14' ext.eddsa_version = '0.2.0' // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: @@ -219,17 +219,15 @@ tasks.withType(Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Controller,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Controller,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = [] } node { - name "CN=Bank A,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Bank A,O=R3,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10012 rpcPort 10013 @@ -237,8 +235,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = [] } node { - name "CN=Bank B,O=R3,OU=corda,L=London,C=UK" - nearestCity "New York" + name "CN=Bank B,O=R3,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10007 rpcPort 10008 @@ -257,7 +254,7 @@ bintrayConfig { projectUrl = 'https://github.com/corda/corda' gpgSign = true gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') - publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver'] + publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] license { name = 'Apache-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0' diff --git a/client/jackson/build.gradle b/client/jackson/build.gradle index 9a7f6a6306..818b742f5d 100644 --- a/client/jackson/build.gradle +++ b/client/jackson/build.gradle @@ -4,6 +4,7 @@ apply plugin: 'net.corda.plugins.publish-utils' dependencies { compile project(':core') + compile project(':finance') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" @@ -23,3 +24,11 @@ dependencies { testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version" testCompile "com.pholser:junit-quickcheck-generators:$quickcheck_version" } + +jar { + baseName 'corda-jackson' +} + +publish { + name = jar.baseName +} \ No newline at end of file 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 100fc2f2f7..eaaeff5306 100644 --- a/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt +++ b/client/jackson/src/main/kotlin/net/corda/jackson/JacksonSupport.kt @@ -7,8 +7,8 @@ import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer import com.fasterxml.jackson.databind.module.SimpleModule import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import com.fasterxml.jackson.module.kotlin.KotlinModule +import net.corda.contracts.BusinessCalendar 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 @@ -20,10 +20,10 @@ import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.i2p.crypto.eddsa.EdDSAPublicKey -import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.x500.X500Name import java.math.BigDecimal import java.security.PublicKey +import java.time.LocalDate import java.util.* /** @@ -39,29 +39,33 @@ object JacksonSupport { interface PartyObjectMapper { @Deprecated("Use partyFromX500Name instead") fun partyFromName(partyName: String): Party? - fun partyFromPrincipal(principal: X500Name): Party? + fun partyFromX500Name(name: X500Name): Party? fun partyFromKey(owningKey: PublicKey): Party? + fun partiesFromName(query: String): Set } - class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : 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 partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) + override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch) } - class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { + class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : 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 partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) + override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch) } 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 partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException() override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() + override fun partiesFromName(query: String) = throw UnsupportedOperationException() } val cordaModule: Module by lazy { @@ -77,6 +81,7 @@ object JacksonSupport { addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer) addDeserializer(SecureHash::class.java, SecureHashDeserializer()) addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer()) + addSerializer(BusinessCalendar::class.java, CalendarSerializer) addDeserializer(BusinessCalendar::class.java, CalendarDeserializer) // For ed25519 pubkeys @@ -106,17 +111,31 @@ object JacksonSupport { } } - /** Mapper requiring RPC support to deserialise parties from names */ + /** + * Creates a Jackson ObjectMapper that uses RPC to deserialise parties from string names. + * + * If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely + * match an identity known from the network map. If true, the name is matched more leniently but if the match + * is ambiguous a [JsonParseException] is thrown. + */ @JvmStatic @JvmOverloads - fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory)) + fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(), + fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch)) /** For testing or situations where deserialising parties is not required */ @JvmStatic @JvmOverloads fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory)) - /** For testing with an in memory identity service */ + /** + * Creates a Jackson ObjectMapper that uses an [IdentityService] directly inside the node to deserialise parties from string names. + * + * If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely + * match an identity known from the network map. If true, the name is matched more leniently but if the match + * is ambiguous a [JsonParseException] is thrown. + */ @JvmStatic @JvmOverloads - fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory()) = configureMapper(IdentityObjectMapper(identityService, factory)) + fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(), + fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch)) private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply { enable(SerializationFeature.INDENT_OUTPUT) @@ -170,14 +189,21 @@ object JacksonSupport { // 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}") + mapper.partyFromX500Name(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") + val nameMatches = mapper.partiesFromName(parser.text) + if (nameMatches.isEmpty()) { + val key = try { + parsePublicKeyBase58(parser.text) + } catch (e: Exception) { + throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key") + } + mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") + } else if (nameMatches.size == 1) { + nameMatches.first() + } else { + throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ...")) } - mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}") } } } @@ -244,11 +270,30 @@ object JacksonSupport { } } + data class BusinessCalendarWrapper(val holidayDates: List) { + fun toCalendar() = BusinessCalendar(holidayDates) + } + + object CalendarSerializer : JsonSerializer() { + override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) { + val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj } + if(calendarName != null) { + generator.writeString(calendarName) + } else { + generator.writeObject(BusinessCalendarWrapper(obj.holidayDates)) + } + } + } + object CalendarDeserializer : JsonDeserializer() { override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar { return try { - val array = StringArrayDeserializer.instance.deserialize(parser, context) - BusinessCalendar.getInstance(*array) + try { + val array = StringArrayDeserializer.instance.deserialize(parser, context) + BusinessCalendar.getInstance(*array) + } catch (e: Exception) { + parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar() + } } catch (e: Exception) { throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}") } diff --git a/client/jfx/build.gradle b/client/jfx/build.gradle index ba6ae045c5..69f39972c6 100644 --- a/client/jfx/build.gradle +++ b/client/jfx/build.gradle @@ -55,3 +55,11 @@ task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath } + +jar { + baseName 'corda-jfx' +} + +publish { + name = jar.baseName +} \ No newline at end of file diff --git a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt index 5f22eadbd4..678f4b3a50 100644 --- a/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt +++ b/client/jfx/src/integration-test/kotlin/net/corda/client/jfx/NodeMonitorModelTest.kt @@ -28,11 +28,11 @@ import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow -import net.corda.node.driver.driver import net.corda.node.services.network.NetworkMapService import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.driver.driver import net.corda.testing.expect import net.corda.testing.expectEvents import net.corda.testing.node.DriverBasedTest @@ -123,14 +123,14 @@ class NodeMonitorModelTest : DriverBasedTest() { vaultUpdates.expectEvents(isStrict = false) { sequence( // SNAPSHOT - expect { output: Vault.Update -> - require(output.consumed.isEmpty()) { output.consumed.size } - require(output.produced.isEmpty()) { output.produced.size } + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.isEmpty()) { produced.size } }, // ISSUE - expect { output: Vault.Update -> - require(output.consumed.isEmpty()) { output.consumed.size } - require(output.produced.size == 1) { output.produced.size } + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.size == 1) { produced.size } } ) } @@ -207,19 +207,19 @@ class NodeMonitorModelTest : DriverBasedTest() { vaultUpdates.expectEvents { sequence( // SNAPSHOT - expect { output: Vault.Update -> - require(output.consumed.isEmpty()) { output.consumed.size } - require(output.produced.isEmpty()) { output.produced.size } + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.isEmpty()) { produced.size } }, // ISSUE - expect { update -> - require(update.consumed.isEmpty()) { update.consumed.size } - require(update.produced.size == 1) { update.produced.size } + expect { (consumed, produced) -> + require(consumed.isEmpty()) { consumed.size } + require(produced.size == 1) { produced.size } }, // MOVE - expect { update -> - require(update.consumed.size == 1) { update.consumed.size } - require(update.produced.isEmpty()) { update.produced.size } + expect { (consumed, produced) -> + require(consumed.size == 1) { consumed.size } + require(produced.isEmpty()) { produced.size } } ) } @@ -227,14 +227,14 @@ class NodeMonitorModelTest : DriverBasedTest() { stateMachineTransactionMapping.expectEvents { sequence( // ISSUE - expect { mapping -> - require(mapping.stateMachineRunId == issueSmId) - require(mapping.transactionId == issueTx!!.id) + expect { (stateMachineRunId, transactionId) -> + require(stateMachineRunId == issueSmId) + require(transactionId == issueTx!!.id) }, // MOVE - expect { mapping -> - require(mapping.stateMachineRunId == moveSmId) - require(mapping.transactionId == moveTx!!.id) + expect { (stateMachineRunId, transactionId) -> + require(stateMachineRunId == moveSmId) + require(transactionId == moveTx!!.id) } ) } diff --git a/client/mock/build.gradle b/client/mock/build.gradle index adac270b39..3b278fcbba 100644 --- a/client/mock/build.gradle +++ b/client/mock/build.gradle @@ -23,3 +23,11 @@ dependencies { testCompile project(':test-utils') } + +jar { + baseName 'corda-mock' +} + +publish { + name = jar.baseName +} \ No newline at end of file diff --git a/client/rpc/build.gradle b/client/rpc/build.gradle index 95f0ed0fcc..b18b563b0f 100644 --- a/client/rpc/build.gradle +++ b/client/rpc/build.gradle @@ -60,6 +60,7 @@ dependencies { testCompile project(':client:mock') // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':smoke-test-utils') smokeTestCompile project(':finance') smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version" smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version" @@ -76,5 +77,12 @@ task integrationTest(type: Test) { task smokeTest(type: Test) { testClassesDir = sourceSets.smokeTest.output.classesDir classpath = sourceSets.smokeTest.runtimeClasspath - systemProperties['build.dir'] = buildDir } + +jar { + baseName 'corda-rpc' +} + +publish { + name = jar.baseName +} \ No newline at end of file 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 3ecb3eda6e..83d5965028 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,17 +5,19 @@ 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.base.Stopwatch 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.node.driver.poll +import net.corda.testing.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.ArtemisConstants import org.apache.activemq.artemis.api.core.SimpleString import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -24,12 +26,10 @@ 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.* import java.util.concurrent.atomic.AtomicInteger - +import kotlin.concurrent.thread +import kotlin.test.fail class RPCStabilityTests { @@ -218,22 +218,65 @@ class RPCStabilityTests { @Test fun `client reconnects to rebooted server`() { - rpcDriver { - val ops = object : ReconnectOps { - override val protocolVersion = 0 - override fun ping() = "pong" + // TODO: Remove multiple trials when we fix the Artemis bug (which should have its own test(s)). + if (ArtemisConstants::class.java.`package`.implementationVersion == "1.5.3") { + // The test fails maybe 1 in 100 times, so to stay green until we upgrade Artemis, retry if it fails: + for (i in (1..3)) { + try { + `client reconnects to rebooted server`(1) + } catch (e: TimeoutException) { + continue + } + return + } + fail("Test failed 3 times, which is vanishingly unlikely unless something has changed.") + } else { + // We've upgraded Artemis so make the test fail reliably, in the 2.1.0 case that takes 25 trials: + `client reconnects to rebooted server`(25) + } + } + + private fun `client reconnects to rebooted server`(trials: Int) { + rpcDriver { + val coreBurner = thread { + while (!Thread.interrupted()) { + // Spin. + } + } + try { + val ops = object : ReconnectOps { + override val protocolVersion = 0 + override fun ping() = "pong" + } + var 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()) + val background = Executors.newSingleThreadExecutor() + (1..trials).forEach { + System.err.println("Start trial $it of $trials.") + serverFollower.shutdown() + serverFollower = shutdownManager.follower() + startRpcServer(ops = ops, customPort = serverPort).getOrThrow() + serverFollower.unfollow() + val stopwatch = Stopwatch.createStarted() + val pingFuture = background.submit(Callable { + client.ping() // Would also hang in foreground, we need it in background so we can timeout. + }) + assertEquals("pong", pingFuture.getOrThrow(10.seconds)) + System.err.println("Took ${stopwatch.elapsed(TimeUnit.MILLISECONDS)} millis.") + } + background.shutdown() // No point in the hanging case. + clientFollower.shutdown() // Driver would do this after the current server, causing 'legit' failover hang. + } finally { + with(coreBurner) { + interrupt() + join() + } } - 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. } } 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 2fde6fcaaf..86dcf7a7d1 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 @@ -157,7 +157,7 @@ class RPCClientProxyHandler( lifeCycle.requireState(State.UNSTARTED) reaperExecutor = Executors.newScheduledThreadPool( 1, - ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build() + ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build() ) reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate( this::reapObservables, 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 index f7863c4029..50d6b90d9e 100644 --- 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 @@ -1,42 +1,47 @@ 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 com.google.common.hash.Hashing +import com.google.common.hash.HashingInputStream import net.corda.client.rpc.CordaRPCConnection import net.corda.client.rpc.notUsed -import net.corda.core.contracts.* +import net.corda.core.contracts.DOLLARS +import net.corda.core.contracts.POUNDS +import net.corda.core.contracts.SWISS_FRANCS +import net.corda.core.crypto.SecureHash 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.seconds 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 net.corda.smoketesting.NodeConfig +import net.corda.smoketesting.NodeProcess +import org.apache.commons.io.output.NullOutputStream import org.junit.After import org.junit.Before import org.junit.Test +import java.io.FilterInputStream +import java.io.InputStream +import java.util.* +import java.util.concurrent.atomic.AtomicInteger +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals 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 + val timeout = 60.seconds } private lateinit var notary: NodeProcess @@ -55,7 +60,7 @@ class StandaloneCordaRPClientTest { @Before fun setUp() { - notary = factory.create(notaryConfig) + notary = NodeProcess.Factory().create(notaryConfig) connection = notary.connect() rpcProxy = connection.proxy notaryIdentity = fetchNotaryIdentity() @@ -71,17 +76,23 @@ class StandaloneCordaRPClientTest { } @Test - fun `test attachment upload`() { + fun `test attachments`() { 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") + assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash") + + val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it -> + it.copyTo(NullOutputStream()) + SecureHash.SHA256(it.hash().asBytes()) + } + assertEquals(attachment.sha256, hash) } @Test fun `test starting flow`() { rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) } @Test @@ -94,7 +105,7 @@ class StandaloneCordaRPClientTest { log.info("Flow>> $msg") ++trackCount } - handle.returnValue.getOrThrow(ofSeconds(timeout)) + handle.returnValue.getOrThrow(timeout) assertNotEquals(0, trackCount) } @@ -118,7 +129,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) assertEquals(1, updateCount) } @@ -135,7 +146,7 @@ class StandaloneCordaRPClientTest { // Now issue some cash rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) - .returnValue.getOrThrow(ofSeconds(timeout)) + .returnValue.getOrThrow(timeout) assertNotEquals(0, updateCount) // Check that this cash exists in the vault diff --git a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt index 8d1fdcb65b..362f001232 100644 --- a/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt +++ b/client/rpc/src/test/kotlin/net/corda/client/rpc/RPCPerformanceTests.kt @@ -1,34 +1,27 @@ package net.corda.client.rpc -import com.codahale.metrics.ConsoleReporter -import com.codahale.metrics.Gauge -import com.codahale.metrics.JmxReporter -import com.codahale.metrics.MetricRegistry import com.google.common.base.Stopwatch import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.core.messaging.RPCOps import net.corda.core.minutes import net.corda.core.seconds -import net.corda.core.utilities.Rate import net.corda.core.utilities.div -import net.corda.node.driver.ShutdownManager import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.testing.RPCDriverExposedDSLInterface +import net.corda.testing.driver.ShutdownManager import net.corda.testing.measure +import net.corda.testing.performance.startPublishingFixedRateInjector +import net.corda.testing.performance.startReporter +import net.corda.testing.performance.startTightLoopInjector import net.corda.testing.rpcDriver import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized -import java.time.Duration import java.util.* -import java.util.concurrent.* -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicInteger -import java.util.concurrent.locks.ReentrantLock -import javax.management.ObjectName -import kotlin.concurrent.thread -import kotlin.concurrent.withLock +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit @Ignore("Only use this locally for profiling") @RunWith(Parameterized::class) @@ -83,12 +76,13 @@ class RPCPerformanceTests : AbstractRPCTest() { val averageIndividualMs: Double, val Mbps: Double ) + @Test fun `measure Megabytes per second for simple RPCs`() { warmup() val inputOutputSizes = listOf(1024, 4096, 100 * 1024) val overallTraffic = 512 * 1024 * 1024L - measure(inputOutputSizes, (1..5)) { inputOutputSize, N -> + measure(inputOutputSizes, (1..5)) { inputOutputSize, _ -> rpcDriver { val proxy = testProxy( RPCClientConfiguration.default.copy( @@ -105,10 +99,9 @@ class RPCPerformanceTests : AbstractRPCTest() { val numberOfRequests = overallTraffic / (2 * inputOutputSize) val timings = Collections.synchronizedList(ArrayList()) - val executor = Executors.newFixedThreadPool(8) val totalElapsed = Stopwatch.createStarted().apply { - startInjectorWithBoundedQueue( - executor = executor, + startTightLoopInjector( + parallelism = 8, numberOfInjections = numberOfRequests.toInt(), queueBound = 100 ) { @@ -118,7 +111,6 @@ class RPCPerformanceTests : AbstractRPCTest() { timings.add(elapsed) } }.stop().elapsed(TimeUnit.MICROSECONDS) - executor.shutdownNow() SimpleRPCResult( requestPerSecond = 1000000.0 * numberOfRequests.toDouble() / totalElapsed.toDouble(), averageIndividualMs = timings.average() / 1000.0, @@ -134,7 +126,7 @@ class RPCPerformanceTests : AbstractRPCTest() { @Test fun `consumption rate`() { rpcDriver { - val metricRegistry = startReporter() + val metricRegistry = startReporter(shutdownManager) val proxy = testProxy( RPCClientConfiguration.default.copy( reapInterval = 1.seconds, @@ -147,14 +139,13 @@ class RPCPerformanceTests : AbstractRPCTest() { producerPoolBound = 8 ) ) - measurePerformancePublishMetrics( + startPublishingFixedRateInjector( metricRegistry = metricRegistry, parallelism = 8, overallDuration = 5.minutes, injectionRate = 20000L / TimeUnit.SECONDS, queueSizeMetricName = "$mode.QueueSize", workDurationMetricName = "$mode.WorkDuration", - shutdownManager = this.shutdownManager, work = { proxy.ops.simpleReply(ByteArray(4096), 4096) } @@ -176,19 +167,17 @@ class RPCPerformanceTests : AbstractRPCTest() { consumerPoolSize = 1 ) ) - val executor = Executors.newFixedThreadPool(clientParallelism) val numberOfMessages = 1000 val bigSize = 10_000_000 val elapsed = Stopwatch.createStarted().apply { - startInjectorWithBoundedQueue( - executor = executor, + startTightLoopInjector( + parallelism = clientParallelism, numberOfInjections = numberOfMessages, queueBound = 4 ) { proxy.ops.simpleReply(ByteArray(bigSize), 0) } }.stop().elapsed(TimeUnit.MICROSECONDS) - executor.shutdownNow() BigMessagesResult( Mbps = bigSize.toDouble() * numberOfMessages.toDouble() / elapsed * (1000000.0 / (1024.0 * 1024.0)) ) @@ -196,120 +185,3 @@ class RPCPerformanceTests : AbstractRPCTest() { }.forEach(::println) } } - -fun measurePerformancePublishMetrics( - metricRegistry: MetricRegistry, - parallelism: Int, - overallDuration: Duration, - injectionRate: Rate, - queueSizeMetricName: String, - workDurationMetricName: String, - shutdownManager: ShutdownManager, - work: () -> Unit -) { - val workSemaphore = Semaphore(0) - metricRegistry.register(queueSizeMetricName, Gauge { workSemaphore.availablePermits() }) - val workDurationTimer = metricRegistry.timer(workDurationMetricName) - val executor = Executors.newSingleThreadScheduledExecutor() - val workExecutor = Executors.newFixedThreadPool(parallelism) - val timings = Collections.synchronizedList(ArrayList()) - for (i in 1 .. parallelism) { - workExecutor.submit { - try { - while (true) { - workSemaphore.acquire() - workDurationTimer.time { - timings.add( - Stopwatch.createStarted().apply { - work() - }.stop().elapsed(TimeUnit.MICROSECONDS) - ) - } - } - } catch (throwable: Throwable) { - throwable.printStackTrace() - } - } - } - val injector = executor.scheduleAtFixedRate( - { - workSemaphore.release((injectionRate * TimeUnit.SECONDS).toInt()) - }, - 0, - 1, - TimeUnit.SECONDS - ) - shutdownManager.registerShutdown { - injector.cancel(true) - workExecutor.shutdownNow() - executor.shutdownNow() - workExecutor.awaitTermination(1, TimeUnit.SECONDS) - executor.awaitTermination(1, TimeUnit.SECONDS) - } - Thread.sleep(overallDuration.toMillis()) -} - -fun startInjectorWithBoundedQueue( - executor: ExecutorService, - numberOfInjections: Int, - queueBound: Int, - work: () -> Unit -) { - val remainingLatch = CountDownLatch(numberOfInjections) - val queuedCount = AtomicInteger(0) - val lock = ReentrantLock() - val canQueueAgain = lock.newCondition() - val injectorShutdown = AtomicBoolean(false) - val injector = thread(name = "injector") { - while (true) { - if (injectorShutdown.get()) break - executor.submit { - work() - if (queuedCount.decrementAndGet() < queueBound / 2) { - lock.withLock { - canQueueAgain.signal() - } - } - remainingLatch.countDown() - } - if (queuedCount.incrementAndGet() > queueBound) { - lock.withLock { - canQueueAgain.await() - } - } - } - } - remainingLatch.await() - injectorShutdown.set(true) - injector.join() -} - -fun RPCDriverExposedDSLInterface.startReporter(): MetricRegistry { - val metricRegistry = MetricRegistry() - val jmxReporter = thread { - JmxReporter. - forRegistry(metricRegistry). - inDomain("net.corda"). - createsObjectNamesWith { _, domain, name -> - // Make the JMX hierarchy a bit better organised. - val category = name.substringBefore('.') - val subName = name.substringAfter('.', "") - if (subName == "") - ObjectName("$domain:name=$category") - else - ObjectName("$domain:type=$category,name=$subName") - }. - build(). - start() - } - val consoleReporter = thread { - ConsoleReporter.forRegistry(metricRegistry).build().start(1, TimeUnit.SECONDS) - } - shutdownManager.registerShutdown { - jmxReporter.interrupt() - consoleReporter.interrupt() - jmxReporter.join() - consoleReporter.join() - } - return metricRegistry -} diff --git a/config/dev/generalnodea.conf b/config/dev/generalnodea.conf index 1ad2dea3d2..94b08cfc0c 100644 --- a/config/dev/generalnodea.conf +++ b/config/dev/generalnodea.conf @@ -1,5 +1,4 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10002" @@ -8,6 +7,6 @@ webAddress : "localhost:10004" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK" + legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" } useHTTPS : false diff --git a/config/dev/generalnodeb.conf b/config/dev/generalnodeb.conf index 510b3e9b70..0fe9e66d9c 100644 --- a/config/dev/generalnodeb.conf +++ b/config/dev/generalnodeb.conf @@ -1,5 +1,4 @@ -myLegalName : "CN=Bank B,O=Bank A,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Bank B,O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10005" @@ -8,6 +7,6 @@ webAddress : "localhost:10007" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "localhost:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK" + legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" } useHTTPS : false diff --git a/config/dev/nameservernode.conf b/config/dev/nameservernode.conf index 73591fa46d..154f88aad9 100644 --- a/config/dev/nameservernode.conf +++ b/config/dev/nameservernode.conf @@ -1,5 +1,4 @@ -myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:10000" diff --git a/constants.properties b/constants.properties index 29575b5763..1676023e4a 100644 --- a/constants.properties +++ b/constants.properties @@ -1,5 +1,5 @@ -gradlePluginsVersion=0.12.2 -kotlinVersion=1.1.2 +gradlePluginsVersion=0.12.4 +kotlinVersion=1.1.1 guavaVersion=21.0 -bouncycastleVersion=1.56 +bouncycastleVersion=1.57 typesafeConfigVersion=1.3.1 diff --git a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java index 4e73dac456..66e0ba9ca0 100644 --- a/cordform-common/src/main/java/net/corda/cordform/CordformNode.java +++ b/cordform-common/src/main/java/net/corda/cordform/CordformNode.java @@ -55,15 +55,6 @@ public class CordformNode { 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. * diff --git a/core/build.gradle b/core/build.gradle index f0029a6672..4f972786e7 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -76,7 +76,7 @@ dependencies { compile "io.requery:requery-kotlin:$requery_version" // For AMQP serialisation. - compile "org.apache.qpid:proton-j:0.18.0" + compile "org.apache.qpid:proton-j:0.19.0" } configurations { @@ -91,3 +91,11 @@ task testJar(type: Jar) { artifacts { testArtifacts testJar } + +jar { + baseName 'corda-core' +} + +publish { + name = jar.baseName +} diff --git a/core/src/main/kotlin/net/corda/core/Streams.kt b/core/src/main/kotlin/net/corda/core/Streams.kt new file mode 100644 index 0000000000..2f33522c35 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/Streams.kt @@ -0,0 +1,30 @@ +package net.corda.core + +import java.util.* +import java.util.Spliterator.* +import java.util.stream.IntStream +import java.util.stream.Stream +import java.util.stream.StreamSupport +import kotlin.streams.asSequence + +private fun IntProgression.spliteratorOfInt(): Spliterator.OfInt { + val kotlinIterator = iterator() + val javaIterator = object : PrimitiveIterator.OfInt { + override fun nextInt() = kotlinIterator.nextInt() + override fun hasNext() = kotlinIterator.hasNext() + override fun remove() = throw UnsupportedOperationException("remove") + } + val spliterator = Spliterators.spliterator( + javaIterator, + (1 + (last - first) / step).toLong(), + SUBSIZED or IMMUTABLE or NONNULL or SIZED or ORDERED or SORTED or DISTINCT + ) + return if (step > 0) spliterator else object : Spliterator.OfInt by spliterator { + override fun getComparator() = Comparator.reverseOrder() + } +} + +fun IntProgression.stream(): IntStream = StreamSupport.intStream(spliteratorOfInt(), false) + +@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable). +inline fun Stream.toTypedArray() = toArray { size -> arrayOfNulls(size) } as Array diff --git a/core/src/main/kotlin/net/corda/core/Utils.kt b/core/src/main/kotlin/net/corda/core/Utils.kt index 89fd25e6e9..2c26c227c2 100644 --- a/core/src/main/kotlin/net/corda/core/Utils.kt +++ b/core/src/main/kotlin/net/corda/core/Utils.kt @@ -24,7 +24,6 @@ import java.nio.file.* import java.nio.file.attribute.FileAttribute import java.time.Duration import java.time.temporal.Temporal -import java.util.HashMap import java.util.concurrent.* import java.util.concurrent.locks.ReentrantLock import java.util.function.BiConsumer @@ -33,8 +32,8 @@ import java.util.zip.Deflater import java.util.zip.ZipEntry import java.util.zip.ZipInputStream import java.util.zip.ZipOutputStream -import kotlin.collections.LinkedHashMap import kotlin.concurrent.withLock +import kotlin.reflect.KClass import kotlin.reflect.KProperty val Int.days: Duration get() = Duration.ofDays(this.toLong()) @@ -106,20 +105,11 @@ fun ListenableFuture.failure(executor: Executor, body: (Throwable) -> Uni infix fun ListenableFuture.then(body: () -> Unit): ListenableFuture = apply { then(RunOnCallerThread, body) } infix fun ListenableFuture.success(body: (T) -> Unit): ListenableFuture = apply { success(RunOnCallerThread, body) } infix fun ListenableFuture.failure(body: (Throwable) -> Unit): ListenableFuture = apply { failure(RunOnCallerThread, body) } +fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) } @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) = mapToArray(transform, iterator(), size) -inline fun IntProgression.mapToArray(transform: (Int) -> R) = mapToArray(transform, iterator(), 1 + (last - first) / step) -inline fun mapToArray(transform: (T) -> R, iterator: Iterator, size: Int) = run { - 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 { @@ -141,12 +131,18 @@ 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) = resolve(other) -operator fun String.div(other: String) = Paths.get(this) / other +operator fun Path.div(other: String): Path = resolve(other) +operator fun String.div(other: String): Path = 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) fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options) +fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path { + require(targetDir.isDirectory()) { "$targetDir is not a directory" } + val targetFile = targetDir.resolve(fileName) + Files.copy(this, targetFile, *options) + return targetFile +} fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options) fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options) fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options) @@ -472,4 +468,25 @@ fun Class.checkNotUnorderedHashMap() { if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) { throw NotSerializableException("Map type $this is unstable under iteration. Suggested fix: use LinkedHashMap instead.") } -} \ No newline at end of file +} + +fun Class<*>.requireExternal(msg: String = "Internal class") + = require(!name.startsWith("net.corda.node.") && !name.contains(".internal.")) { "$msg: $name" } + +interface DeclaredField { + companion object { + inline fun Any?.declaredField(clazz: KClass<*>, name: String): DeclaredField = declaredField(clazz.java, name) + inline fun Any.declaredField(name: String): DeclaredField = declaredField(javaClass, name) + inline fun Any?.declaredField(clazz: Class<*>, name: String): DeclaredField { + val javaField = clazz.getDeclaredField(name).apply { isAccessible = true } + val receiver = this + return object : DeclaredField { + override var value + get() = javaField.get(receiver) as T + set(value) = javaField.set(receiver, value) + } + } + } + + var value: T +} diff --git a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt similarity index 56% rename from core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt rename to core/src/main/kotlin/net/corda/core/contracts/Amount.kt index 9ccff91150..0569c6751c 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/FinanceTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Amount.kt @@ -1,20 +1,8 @@ package net.corda.core.contracts -import com.fasterxml.jackson.core.JsonGenerator -import com.fasterxml.jackson.core.JsonParser -import com.fasterxml.jackson.databind.DeserializationContext -import com.fasterxml.jackson.databind.JsonDeserializer -import com.fasterxml.jackson.databind.JsonSerializer -import com.fasterxml.jackson.databind.SerializerProvider -import com.fasterxml.jackson.databind.annotation.JsonDeserialize -import com.fasterxml.jackson.databind.annotation.JsonSerialize -import com.google.common.annotations.VisibleForTesting import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.math.RoundingMode -import java.time.DayOfWeek -import java.time.LocalDate -import java.time.format.DateTimeFormatter import java.util.* /** @@ -452,394 +440,3 @@ class AmountTransfer(val quantityDelta: Long, } } - -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -// -// Interest rate fixes -// -//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - -/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ -@CordaSerializable -data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) - -/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ -data class Fix(val of: FixOf, val value: BigDecimal) : CommandData - -/** Represents a textual expression of e.g. a formula */ -@CordaSerializable -@JsonDeserialize(using = ExpressionDeserializer::class) -@JsonSerialize(using = ExpressionSerializer::class) -data class Expression(val expr: String) - -object ExpressionSerializer : JsonSerializer() { - override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) { - generator.writeString(expr.expr) - } -} - -object ExpressionDeserializer : JsonDeserializer() { - override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression { - return Expression(parser.text) - } -} - -/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */ -@CordaSerializable -data class Tenor(val name: String) { - private val amount: Int - private val unit: TimeUnit - - init { - if (name == "ON") { - // Overnight - amount = 1 - unit = TimeUnit.Day - } else { - val regex = """(\d+)([DMYW])""".toRegex() - val match = regex.matchEntire(name)?.groupValues ?: throw IllegalArgumentException("Unrecognised tenor name: $name") - - amount = match[1].toInt() - unit = TimeUnit.values().first { it.code == match[2] } - } - } - - fun daysToMaturity(startDate: LocalDate, calendar: BusinessCalendar): Int { - val maturityDate = when (unit) { - TimeUnit.Day -> startDate.plusDays(amount.toLong()) - TimeUnit.Week -> startDate.plusWeeks(amount.toLong()) - TimeUnit.Month -> startDate.plusMonths(amount.toLong()) - TimeUnit.Year -> startDate.plusYears(amount.toLong()) - else -> throw IllegalStateException("Invalid tenor time unit: $unit") - } - // Move date to the closest business day when it falls on a weekend/holiday - val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing) - val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual) - - return daysToMaturity - } - - override fun toString(): String = name - - @CordaSerializable - enum class TimeUnit(val code: String) { - Day("D"), Week("W"), Month("M"), Year("Y") - } -} - -/** - * Simple enum for returning accurals adjusted or unadjusted. - * We don't actually do anything with this yet though, so it's ignored for now. - */ -@CordaSerializable -enum class AccrualAdjustment { - Adjusted, Unadjusted -} - -/** - * This is utilised in the [DateRollConvention] class to determine which way we should initially step when - * finding a business day. - */ -@CordaSerializable -enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) } - -/** - * This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day. - * Depending on the accounting requirement, we can move forward until we get to a business day, or backwards. - * There are some additional rules which are explained in the individual cases below. - */ -@CordaSerializable -enum class DateRollConvention(val direction: () -> DateRollDirection, val isModified: Boolean) { - // direction() cannot be a val due to the throw in the Actual instance - - /** Don't roll the date, use the one supplied. */ - Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false), - /** Following is the next business date from this one. */ - Following({ DateRollDirection.FORWARD }, false), - /** - * "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding - * business date. - */ - ModifiedFollowing({ DateRollDirection.FORWARD }, true), - /** Previous is the previous business date from this one. */ - Previous({ DateRollDirection.BACKWARD }, false), - /** - * Modified previous is the previous business date, unless it's in the previous month, in which case use the next - * business date. - */ - ModifiedPrevious({ DateRollDirection.BACKWARD }, true); -} - - -/** - * This forms the day part of the "Day Count Basis" used for interest calculation. - * Note that the first character cannot be a number (enum naming constraints), so we drop that - * in the toString lest some people get confused. - */ -@CordaSerializable -enum class DayCountBasisDay { - // We have to prefix 30 etc with a letter due to enum naming constraints. - D30, - D30N, D30P, D30E, D30G, DActual, DActualJ, D30Z, D30F, DBus_SaoPaulo; - - override fun toString(): String { - return super.toString().drop(1) - } -} - -/** This forms the year part of the "Day Count Basis" used for interest calculation. */ -@CordaSerializable -enum class DayCountBasisYear { - // Ditto above comment for years. - Y360, - Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YICMA, Y252; - - override fun toString(): String { - return super.toString().drop(1) - } -} - -/** Whether the payment should be made before the due date, or after it. */ -@CordaSerializable -enum class PaymentRule { - InAdvance, InArrears, -} - -/** - * Frequency at which an event occurs - the enumerator also casts to an integer specifying the number of times per year - * that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc). - */ -@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed. -@CordaSerializable -enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) { - Annual(1, { plusYears(1 * it) }), - SemiAnnual(2, { plusMonths(6 * it) }), - Quarterly(4, { plusMonths(3 * it) }), - Monthly(12, { plusMonths(1 * it) }), - Weekly(52, { plusWeeks(1 * it) }), - BiWeekly(26, { plusWeeks(2 * it) }), - Daily(365, { plusDays(1 * it) }); -} - - -@Suppress("unused") // This utility may be useful in future. TODO: Review before API stability guarantees in place. -fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = accordingToCalendar.isWorkingDay(this) - -// TODO: Make Calendar data come from an oracle - -/** - * A business calendar performs date calculations that take into account national holidays and weekends. This is a - * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when - * no staff are around to handle problems. - */ -@CordaSerializable -open class BusinessCalendar private constructor(val holidayDates: List) { - @CordaSerializable - class UnknownCalendar(name: String) : Exception("$name not found") - - companion object { - val calendars = listOf("London", "NewYork") - - val TEST_CALENDAR_DATA = calendars.map { - it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText() - }.toMap() - - /** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ - fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) - - /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */ - fun getInstance(vararg calname: String) = BusinessCalendar( - calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. - toSet(). - map { parseDateFromString(it) }. - toList().sorted() - ) - - /** Calculates an event schedule that moves events around to ensure they fall on working days. */ - fun createGenericSchedule(startDate: LocalDate, - period: Frequency, - calendar: BusinessCalendar = getInstance(), - dateRollConvention: DateRollConvention = DateRollConvention.Following, - noOfAdditionalPeriods: Int = Integer.MAX_VALUE, - endDate: LocalDate? = null, - periodOffset: Int? = null): List { - val ret = ArrayList() - var ctr = 0 - var currentDate = startDate - - while (true) { - currentDate = getOffsetDate(currentDate, period) - if (periodOffset == null || periodOffset <= ctr) - ret.add(calendar.applyRollConvention(currentDate, dateRollConvention)) - ctr += 1 - // TODO: Fix addl period logic - if ((ctr > noOfAdditionalPeriods) || (currentDate >= endDate ?: currentDate)) - break - } - return ret - } - - /** Calculates the date from @startDate moving forward @steps of time size @period. Does not apply calendar - * logic / roll conventions. - */ - fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate { - if (steps == 0) return startDate - return period.offset(startDate, steps.toLong()) - } - } - - override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) { - /** Note this comparison is OK as we ensure they are sorted in getInstance() */ - this.holidayDates == other.holidayDates - } else { - false - } - - override fun hashCode(): Int { - return this.holidayDates.hashCode() - } - - open fun isWorkingDay(date: LocalDate): Boolean = - when { - date.dayOfWeek == DayOfWeek.SATURDAY -> false - date.dayOfWeek == DayOfWeek.SUNDAY -> false - holidayDates.contains(date) -> false - else -> true - } - - open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate { - if (dateRollConvention == DateRollConvention.Actual) return testDate - - var direction = dateRollConvention.direction().value - var trialDate = testDate - while (!isWorkingDay(trialDate)) { - trialDate = trialDate.plusDays(direction) - } - - // We've moved to the next working day in the right direction, but if we're using the "modified" date roll - // convention and we've crossed into another month, reverse the direction instead to stay within the month. - // Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp - - if (dateRollConvention.isModified && testDate.month != trialDate.month) { - direction = -direction - trialDate = testDate - while (!isWorkingDay(trialDate)) { - trialDate = trialDate.plusDays(direction) - } - } - return trialDate - } - - /** - * Returns a date which is the inbound date plus/minus a given number of business days. - * TODO: Make more efficient if necessary - */ - fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate { - require(i >= 0) - if (i == 0) return date - var retDate = date - var ctr = 0 - while (ctr < i) { - retDate = retDate.plusDays(direction.value) - if (isWorkingDay(retDate)) ctr++ - } - return retDate - } -} - -fun calculateDaysBetween(startDate: LocalDate, - endDate: LocalDate, - dcbYear: DayCountBasisYear, - dcbDay: DayCountBasisDay): Int { - // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. - // TODO: The rest. - return when { - dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() - dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() - else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") - } -} - -/** - * Enum for the types of netting that can be applied to state objects. Exact behaviour - * for each type of netting is left to the contract to determine. - */ -@CordaSerializable -enum class NetType { - /** - * Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific), - * and allows their counterparty to net obligations without requiring approval from all parties. For example, if - * Bank A owes Bank B £1m, and Bank B owes Bank A £1m, in the case of Bank B defaulting this would enable Bank A - * to net out the two obligations to zero, rather than being legally obliged to pay £1m without any realistic - * expectation of the debt to them being paid. Realistically this is limited to bilateral netting, to simplify - * determining which party must sign the netting transaction. - */ - CLOSE_OUT, - /** - * "Payment" is used to refer to conventional netting, where all parties must confirm the netting transaction. This - * can be a multilateral netting transaction, and may be created by a central clearing service. - */ - PAYMENT -} - -/** - * Class representing a commodity, as an equivalent to the [Currency] class. This exists purely to enable the - * [CommodityContract] contract, and is likely to change in future. - * - * @param commodityCode a unique code for the commodity. No specific registry for these is currently defined, although - * this is likely to change in future. - * @param displayName human readable name for the commodity. - * @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of - * this commodity. - */ -@CordaSerializable -data class Commodity(val commodityCode: String, - val displayName: String, - val defaultFractionDigits: Int = 0) : TokenizableAssetInfo { - override val displayTokenSize: BigDecimal - get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits) - - companion object { - private val registry = mapOf( - // Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp - Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice")) - ) - - fun getInstance(commodityCode: String): Commodity? - = registry[commodityCode] - } -} - -/** - * This class provides a truly unique identifier of a trade, state, or other business object, bound to any existing - * external ID. Equality and comparison are based on the unique ID only; if two states somehow have the same UUID but - * different external IDs, it would indicate a problem with handling of IDs. - * - * @param externalId Any existing weak identifier such as trade reference ID. - * This should be set here the first time a [UniqueIdentifier] is created as part of state issuance, - * or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong ID. - * @param id Should never be set by user code and left as default initialised. - * So that the first time a state is issued this should be given a new UUID. - * Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified. - */ -@CordaSerializable -data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { - override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() - - companion object { - /** Helper function for unit tests where the UUID needs to be manually initialised for consistency. */ - @VisibleForTesting - fun fromString(name: String): UniqueIdentifier = UniqueIdentifier(null, UUID.fromString(name)) - } - - override fun compareTo(other: UniqueIdentifier): Int = id.compareTo(other.id) - - override fun equals(other: Any?): Boolean { - return if (other is UniqueIdentifier) - id == other.id - else - false - } - - override fun hashCode(): Int = id.hashCode() -} 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 437dcb4e96..4446c3c04f 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/ContractsDSL.kt @@ -3,8 +3,8 @@ package net.corda.core.contracts import net.corda.core.identity.Party -import java.security.PublicKey import java.math.BigDecimal +import java.security.PublicKey import java.util.* /** @@ -22,15 +22,12 @@ import java.util.* fun currency(code: String) = Currency.getInstance(code)!! -fun commodity(code: String) = Commodity.getInstance(code)!! - @JvmField val USD = currency("USD") @JvmField val GBP = currency("GBP") @JvmField val EUR = currency("EUR") @JvmField val CHF = currency("CHF") @JvmField val JPY = currency("JPY") @JvmField val RUB = currency("RUB") -@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum! fun AMOUNT(amount: Int, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token) fun AMOUNT(amount: Double, token: T): Amount = Amount.fromDecimal(BigDecimal.valueOf(amount), token) @@ -38,19 +35,15 @@ fun DOLLARS(amount: Int): Amount = AMOUNT(amount, USD) fun DOLLARS(amount: Double): Amount = AMOUNT(amount, USD) fun POUNDS(amount: Int): Amount = AMOUNT(amount, GBP) fun SWISS_FRANCS(amount: Int): Amount = AMOUNT(amount, CHF) -fun FCOJ(amount: Int): Amount = AMOUNT(amount, FCOJ) val Int.DOLLARS: Amount get() = DOLLARS(this) val Double.DOLLARS: Amount get() = DOLLARS(this) val Int.POUNDS: Amount get() = POUNDS(this) val Int.SWISS_FRANCS: Amount get() = SWISS_FRANCS(this) -val Int.FCOJ: Amount get() = FCOJ(this) infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) -infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Amount.`issued by`(deposit: PartyAndReference) = issuedBy(deposit) infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) -infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this) infix fun Amount.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit)) //// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// 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 5f687b2fb4..d10d9cc2bb 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/Structures.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/Structures.kt @@ -5,11 +5,8 @@ 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 import net.corda.core.serialization.* -import net.corda.core.transactions.TransactionBuilder import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream @@ -24,37 +21,7 @@ interface NamedByHash { val id: SecureHash } -/** - * Interface for state objects that support being netted with other state objects. - */ -interface BilateralNettableState> { - /** - * Returns an object used to determine if two states can be subject to close-out netting. If two states return - * equal objects, they can be close out netted together. - */ - val bilateralNetState: Any - - /** - * Perform bilateral netting of this state with another state. The two states must be compatible (as in - * bilateralNetState objects are equal). - */ - fun net(other: N): N -} - -/** - * Interface for state objects that support being netted with other state objects. - */ -interface MultilateralNettableState { - /** - * Returns an object used to determine if two states can be subject to close-out netting. If two states return - * equal objects, they can be close out netted together. - */ - val multilateralNetState: T -} - -interface NettableState, out T : Any> : BilateralNettableState, - MultilateralNettableState - +// DOCSTART 1 /** * A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk * file that the program can use to persist data across transactions. States are immutable: once created they are never @@ -117,7 +84,9 @@ interface ContractState { */ val participants: List } +// DOCEND 1 +// DOCSTART 4 /** * A wrapper for [ContractState] containing additional platform-level state information. * This is the definitive state that is stored on the ledger and used in transaction outputs. @@ -146,6 +115,7 @@ data class TransactionState @JvmOverloads constructor( * otherwise the transaction is not valid. */ val encumbrance: Int? = null) +// DOCEND 4 /** Wraps the [ContractState] in a [TransactionState] object */ infix fun T.`with notary`(newNotary: Party) = withNotary(newNotary) @@ -170,6 +140,7 @@ data class Issued(val issuer: PartyAndReference, val product: P) { */ fun Amount>.withoutIssuer(): Amount = Amount(quantity, token.product) +// DOCSTART 3 /** * A contract state that can have a single owner. */ @@ -180,6 +151,7 @@ interface OwnableState : ContractState { /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ fun withNewOwner(newOwner: AbstractParty): Pair } +// DOCEND 3 /** Something which is scheduled to happen at a point in time */ interface Scheduled { @@ -208,6 +180,7 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan */ data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledAt: Instant) : Scheduled +// DOCSTART 2 /** * A state that evolves by superseding itself, all of which share the common "linearId". * @@ -246,6 +219,7 @@ interface LinearState : ContractState { } } } +// DOCEND 2 interface SchedulableState : ContractState { /** @@ -260,64 +234,6 @@ interface SchedulableState : ContractState { fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? } -/** - * Interface representing an agreement that exposes various attributes that are common. Implementing it simplifies - * implementation of general flows that manipulate many agreement types. - */ -interface DealState : LinearState { - /** Human readable well known reference (e.g. trade reference) */ - val ref: String - - /** - * Exposes the Parties involved in a generic way. - * - * Appears to duplicate [participants] a property of [ContractState]. However [participants] only holds public keys. - * Currently we need to hard code Party objects into [ContractState]s. [Party] objects are a wrapper for public - * keys which also contain some identity information about the public key owner. You can keep track of individual - * parties by adding a property for each one to the state, or you can append parties to the [parties] list if you - * are implementing [DealState]. We need to do this as identity management in Corda is currently incomplete, - * therefore the only way to record identity information is in the [ContractState]s themselves. When identity - * management is completed, parties to a transaction will only record public keys in the [DealState] and through a - * separate process exchange certificates to ascertain identities. Thus decoupling identities from - * [ContractState]s. - * */ - val parties: List - - /** - * Generate a partial transaction representing an agreement (command) to this deal, allowing a general - * deal/agreement flow to generate the necessary transaction for potential implementations. - * - * TODO: Currently this is the "inception" transaction but in future an offer of some description might be an input state ref - * - * TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a - * Contract instance from a ContractState are imminent, at which point we can move this out of here. - */ - fun generateAgreement(notary: Party): TransactionBuilder -} - -/** - * Interface adding fixing specific methods. - */ -interface FixableDealState : DealState { - /** - * When is the next fixing and what is the fixing for? - */ - fun nextFixingOf(): FixOf? - - /** - * What oracle service to use for the fixing - */ - val oracleType: ServiceType - - /** - * Generate a fixing command for this deal and fix. - * - * TODO: This would also likely move to methods on the Contract once the changes to reference - * the Contract from the ContractState are in. - */ - fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) -} - /** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes) @@ -326,13 +242,17 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes) * transaction defined the state and where in that transaction it was. */ @CordaSerializable +// DOCSTART 8 data class StateRef(val txhash: SecureHash, val index: Int) { override fun toString() = "$txhash($index)" } +// DOCEND 8 /** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */ @CordaSerializable +// DOCSTART 7 data class StateAndRef(val state: TransactionState, val ref: StateRef) +// DOCEND 7 /** Filters a list of [StateAndRef] objects according to the type of the states */ inline fun Iterable>.filterStatesOfType(): List> { @@ -360,7 +280,9 @@ abstract class TypeOnlyCommandData : CommandData { /** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */ @CordaSerializable +// DOCSTART 9 data class Command(val value: CommandData, val signers: List) { +// DOCEND 9 init { require(signers.isNotEmpty()) } @@ -387,15 +309,10 @@ interface MoveCommand : CommandData { val contractHash: SecureHash? } -/** A common netting command for contracts whose states can be netted. */ -interface NetCommand : CommandData { - /** The type of netting to apply, see [NetType] for options. */ - val type: NetType -} - /** Indicates that this transaction replaces the inputs contract state to another contract state */ data class UpgradeCommand(val upgradedContractClass: Class>) : CommandData +// DOCSTART 6 /** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */ @CordaSerializable data class AuthenticatedObject( @@ -404,35 +321,71 @@ data class AuthenticatedObject( val signingParties: List, val value: T ) +// DOCEND 6 /** + * A time-window is required for validation/notarization purposes. * If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be - * between (after, before). + * between (fromTime, untilTime). + * Usually, a time-window is required to have both sides set (fromTime, untilTime). + * However, some apps may require that a time-window has a start [Instant] (fromTime), but no end [Instant] (untilTime) and vice versa. + * TODO: Consider refactoring using TimeWindow abstraction like TimeWindow.From, TimeWindow.Until, TimeWindow.Between. */ @CordaSerializable -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? +class TimeWindow private constructor( + /** The time at which this transaction is said to have occurred is after this moment. */ + val fromTime: Instant?, + /** The time at which this transaction is said to have occurred is before this moment. */ + val untilTime: Instant? ) { - init { - if (after == null && before == null) - throw IllegalArgumentException("At least one of before/after must be specified") - if (after != null && before != null) - check(after <= before) + companion object { + /** Use when the left-side [fromTime] of a [TimeWindow] is only required and we don't need an end instant (untilTime). */ + @JvmStatic + fun fromOnly(fromTime: Instant) = TimeWindow(fromTime, null) + + /** Use when the right-side [untilTime] of a [TimeWindow] is only required and we don't need a start instant (fromTime). */ + @JvmStatic + fun untilOnly(untilTime: Instant) = TimeWindow(null, untilTime) + + /** Use when both sides of a [TimeWindow] must be set ([fromTime], [untilTime]). */ + @JvmStatic + fun between(fromTime: Instant, untilTime: Instant): TimeWindow { + require(fromTime < untilTime) { "fromTime should be earlier than untilTime" } + return TimeWindow(fromTime, untilTime) + } + + /** Use when we have a start time and a period of validity. */ + @JvmStatic + fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration) + + /** + * When we need to create a [TimeWindow] based on a specific time [Instant] and some tolerance in both sides of this instant. + * The result will be the following time-window: ([time] - [tolerance], [time] + [tolerance]). + */ + @JvmStatic + fun withTolerance(time: Instant, tolerance: Duration) = between(time - tolerance, time + tolerance) } - constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance) + /** The midpoint is calculated as fromTime + (untilTime - fromTime)/2. Note that it can only be computed if both sides are set. */ + val midpoint: Instant get() = fromTime!! + Duration.between(fromTime, untilTime!!).dividedBy(2) - val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is TimeWindow) return false + return (fromTime == other.fromTime && untilTime == other.untilTime) + } + + override fun hashCode() = 31 * (fromTime?.hashCode() ?: 0) + (untilTime?.hashCode() ?: 0) + + override fun toString() = "TimeWindow(fromTime=$fromTime, untilTime=$untilTime)" } +// DOCSTART 5 /** * Implemented by a program that implements business logic on the shared ledger. All participants run this code for * every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the * transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted - * timestamp attached to the transaction itself i.e. it is NOT necessarily the current time. + * time-window attached to the transaction itself i.e. it is NOT necessarily the current time. * * TODO: Contract serialization is likely to change, so the annotation is likely temporary. */ @@ -453,6 +406,7 @@ interface Contract { */ val legalContractReference: SecureHash } +// DOCEND 5 /** * Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract. @@ -510,7 +464,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { val storage = serviceHub.storageService.attachments return { val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) - if (a is AbstractAttachment) a.attachmentData else a.open().use { it.readBytes() } + (a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() } } } } 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 e4d343a3c6..b1e98a49e9 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionTypes.kt @@ -19,7 +19,7 @@ sealed class TransactionType { */ @Throws(TransactionVerificationException::class) fun verify(tx: LedgerTransaction) { - require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." } + require(tx.notary != null || tx.timeWindow == null) { "Transactions with time-windows must be notarised" } val duplicates = detectDuplicateInputs(tx) if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates) val missing = verifySigners(tx) diff --git a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt index 8ad833d7af..5417169459 100644 --- a/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt +++ b/core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt @@ -1,8 +1,8 @@ package net.corda.core.contracts -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException +import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import java.security.PublicKey import java.util.* @@ -13,13 +13,15 @@ import java.util.* * A transaction to be passed as input to a contract verification function. Defines helper methods to * simplify verification logic in contracts. */ +// DOCSTART 1 data class TransactionForContract(val inputs: List, val outputs: List, val attachments: List, val commands: List>, val origHash: SecureHash, val inputNotary: Party? = null, - val timestamp: Timestamp? = null) { + val timeWindow: TimeWindow? = null) { +// DOCEND 1 override fun hashCode() = origHash.hashCode() override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash @@ -37,6 +39,7 @@ data class TransactionForContract(val inputs: List, * currency. To solve this, you would use groupStates with a type of Cash.State and a selector that returns the * currency field: the resulting list can then be iterated over to perform the per-currency calculation. */ + // DOCSTART 2 fun groupStates(ofType: Class, selector: (T) -> K): List> { val inputs = inputs.filterIsInstance(ofType) val outputs = outputs.filterIsInstance(ofType) @@ -47,6 +50,7 @@ data class TransactionForContract(val inputs: List, @Suppress("DEPRECATION") return groupStatesInternal(inGroups, outGroups) } + // DOCEND 2 /** See the documentation for the reflection-based version of [groupStates] */ inline fun groupStates(selector: (T) -> K): List> { @@ -83,7 +87,9 @@ data class TransactionForContract(val inputs: List, * up on both sides of the transaction, but the values must be summed independently per currency. Grouping can * be used to simplify this logic. */ + // DOCSTART 3 data class InOutGroup(val inputs: List, val outputs: List, val groupingKey: K) + // DOCEND 3 } class TransactionResolutionException(val hash: SecureHash) : FlowException() { diff --git a/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt new file mode 100644 index 0000000000..2be023d047 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/contracts/UniqueIdentifier.kt @@ -0,0 +1,39 @@ +package net.corda.core.contracts + +import com.google.common.annotations.VisibleForTesting +import net.corda.core.serialization.CordaSerializable +import java.util.* + +/** + * This class provides a truly unique identifier of a trade, state, or other business object, bound to any existing + * external ID. Equality and comparison are based on the unique ID only; if two states somehow have the same UUID but + * different external IDs, it would indicate a problem with handling of IDs. + * + * @param externalId Any existing weak identifier such as trade reference ID. + * This should be set here the first time a [UniqueIdentifier] is created as part of state issuance, + * or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong ID. + * @param id Should never be set by user code and left as default initialised. + * So that the first time a state is issued this should be given a new UUID. + * Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified. + */ +@CordaSerializable +data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable { + override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString() + + companion object { + /** Helper function for unit tests where the UUID needs to be manually initialised for consistency. */ + @VisibleForTesting + fun fromString(name: String): UniqueIdentifier = UniqueIdentifier(null, UUID.fromString(name)) + } + + override fun compareTo(other: UniqueIdentifier): Int = id.compareTo(other.id) + + override fun equals(other: Any?): Boolean { + return if (other is UniqueIdentifier) + id == other.id + else + false + } + + override fun hashCode(): Int = id.hashCode() +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt index eb571f8278..1e0ae94678 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CompositeKey.kt @@ -1,7 +1,9 @@ package net.corda.core.crypto +import net.corda.core.crypto.CompositeKey.NodeAndWeight import net.corda.core.serialization.CordaSerializable -import net.corda.core.serialization.serialize +import org.bouncycastle.asn1.* +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import java.security.PublicKey /** @@ -35,20 +37,24 @@ class CompositeKey private constructor (val threshold: Int, * Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode. */ @CordaSerializable - data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable { + data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable, ASN1Object() { override fun compareTo(other: NodeAndWeight): Int { if (weight == other.weight) { return node.hashCode().compareTo(other.node.hashCode()) } else return weight.compareTo(other.weight) } + + override fun toASN1Primitive(): ASN1Primitive { + val vector = ASN1EncodableVector() + vector.add(DERBitString(node.encoded)) + vector.add(ASN1Integer(weight.toLong())) + return DERSequence(vector) + } } companion object { - // TODO: Get the design standardised and from there define a recognised name - val ALGORITHM = "X-Corda-CompositeKey" - // TODO: We should be using a well defined format. - val FORMAT = "X-Corda-Kryo" + val ALGORITHM = CompositeSignature.ALGORITHM_IDENTIFIER.algorithm.toString() } /** @@ -57,8 +63,17 @@ class CompositeKey private constructor (val threshold: Int, fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key)) override fun getAlgorithm() = ALGORITHM - override fun getEncoded(): ByteArray = this.serialize().bytes - override fun getFormat() = FORMAT + override fun getEncoded(): ByteArray { + val keyVector = ASN1EncodableVector() + val childrenVector = ASN1EncodableVector() + children.forEach { + childrenVector.add(it.toASN1Primitive()) + } + keyVector.add(ASN1Integer(threshold.toLong())) + keyVector.add(DERSequence(childrenVector)) + return SubjectPublicKeyInfo(CompositeSignature.ALGORITHM_IDENTIFIER, DERSequence(keyVector)).encoded + } + override fun getFormat() = ASN1Encoding.DER /** * Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite 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 1f5f7693e2..5bfd46466f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/Crypto.kt @@ -9,6 +9,7 @@ import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.ASN1ObjectIdentifier +import org.bouncycastle.asn1.ASN1Sequence import org.bouncycastle.asn1.DERSequence import org.bouncycastle.asn1.bc.BCObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers @@ -19,8 +20,9 @@ import org.bouncycastle.asn1.x509.Extension import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo import org.bouncycastle.asn1.x9.X9ObjectIdentifiers +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.bc.BcX509ExtensionUtils -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 @@ -29,6 +31,14 @@ 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.jce.spec.ECParameterSpec +import org.bouncycastle.jce.spec.ECPrivateKeySpec +import org.bouncycastle.jce.spec.ECPublicKeySpec +import org.bouncycastle.math.ec.ECConstants +import org.bouncycastle.math.ec.FixedPointCombMultiplier +import org.bouncycastle.math.ec.WNafUtil +import org.bouncycastle.operator.ContentSigner +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider @@ -42,11 +52,12 @@ import java.math.BigInteger import java.security.* import java.security.KeyFactory import java.security.KeyPairGenerator -import java.security.cert.X509Certificate import java.security.spec.InvalidKeySpecException import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.X509EncodedKeySpec import java.util.* +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec /** * This object controls and provides the available and supported signature schemes for Corda. @@ -242,8 +253,7 @@ 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") + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } try { return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { @@ -298,8 +308,7 @@ object Crypto { */ @Throws(IllegalArgumentException::class, InvalidKeySpecException::class) fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } try { return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) } catch (ikse: InvalidKeySpecException) { @@ -345,8 +354,7 @@ object Crypto { */ @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "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!") signature.initSign(privateKey) @@ -425,8 +433,7 @@ object Crypto { */ @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "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!") val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData) @@ -488,8 +495,7 @@ object Crypto { */ @Throws(SignatureException::class, IllegalArgumentException::class) fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) signature.initVerify(publicKey) signature.update(clearData) @@ -516,8 +522,7 @@ object Crypto { @Throws(IllegalArgumentException::class) @JvmOverloads fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) if (signatureScheme.algSpec != null) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) @@ -526,6 +531,143 @@ object Crypto { return keyPairGenerator.generateKeyPair() } + /** + * Deterministically generate/derive a [KeyPair] using an existing private key and a seed as inputs. + * This operation is currently supported for ECDSA secp256r1 (NIST P-256), ECDSA secp256k1 and EdDSA ed25519. + * + * Similarly to BIP32, the implemented algorithm uses an HMAC function based on SHA512 and it is actually + * an implementation the HKDF rfc - Step 1: Extract function, + * @see HKDF + * which is practically a variation of the private-parent-key -> private-child-key hardened key generation of BIP32. + * + * Unlike BIP32, where both private and public keys are extended to prevent deterministically + * generated child keys from depending solely on the key itself, current method uses normal elliptic curve keys + * without a chain-code and the generated key relies solely on the security of the private key. + * + * Although without a chain-code we lose the aforementioned property of not depending solely on the key, + * it should be mentioned that the cryptographic strength of the HMAC depends upon the size of the secret key. + * @see HMAC Security + * Thus, as long as the master key is kept secret and has enough entropy (~256 bits for EC-schemes), the system + * is considered secure. + * + * It is also a fact that if HMAC is used as PRF and/or MAC but not as checksum function, the function is still + * secure even if the underlying hash function is not collision resistant (e.g. if we used MD5). + * In practice, for our DKG purposes (thus PRF), a collision would not necessarily reveal the master HMAC key, + * because multiple inputs can produce the same hash output. + * + * Also according to the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) rfc5869: + *

    + *
  • a chain-code (aka the salt) is recommended, but not required. + *
  • the salt can be public, but a hidden one provides stronger security guarantee. + *
  • even a simple counter can work as a salt, but ideally it should be random. + *
  • salt values should not be chosen by an attacker. + *

+ * + * Regarding the last requirement, according to Krawczyk's HKDF scheme: While there is no need to keep the salt secret, + * it is assumed that salt values are independent of the input keying material. + * @see Cryptographic Extraction and Key Derivation - The HKDF Scheme. + * + * There are also protocols that require an authenticated nonce (e.g. when a DH derived key is used as a seed) and thus + * we need to make sure that nonces come from legitimate parties rather than selected by an attacker. + * Similarly, in DLT systems, proper handling is required if users should agree on a common value as a seed, + * e.g. a transaction's nonce or hash. + * + * Moreover if a unique key per transaction is prerequisite, an attacker should never force a party to reuse a + * previously used key, due to privacy and forward secrecy reasons. + * + * All in all, this algorithm can be used with a counter as seed, however it is suggested that the output does + * not solely depend on the key, i.e. a secret salt per user or a random nonce per transaction could serve this role. + * In case where a non-random seed policy is selected, such as the BIP32 counter logic, one needs to carefully keep state + * so that the same salt is used only once. + * + * @param signatureScheme the [SignatureScheme] of the private key input. + * @param privateKey the [PrivateKey] that will be used as key to the HMAC-ed DKG function. + * @param seed an extra seed that will be used as value to the underlying HMAC. + * @return a new deterministically generated [KeyPair]. + * @throws IllegalArgumentException if the requested signature scheme is not supported. + * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. + */ + fun deterministicKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair { + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } + when (signatureScheme) { + ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed) + EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed) + } + throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}") + } + + /** + * Deterministically generate/derive a [KeyPair] using an existing private key and a seed as inputs. + * Use this method if the [SignatureScheme] of the private key input is not known. + * @param privateKey the [PrivateKey] that will be used as key to the HMAC-ed DKG function. + * @param seed an extra seed that will be used as value to the underlying HMAC. + * @return a new deterministically generated [KeyPair]. + * @throws IllegalArgumentException if the requested signature scheme is not supported. + * @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme. + */ + fun deterministicKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair { + return deterministicKeyPair(findSignatureScheme(privateKey), privateKey, seed) + } + + // Given the domain parameters, this routine deterministically generates an ECDSA key pair + // in accordance with X9.62 section 5.2.1 pages 26, 27. + private fun deriveKeyPairECDSA(parameterSpec: ECParameterSpec, privateKey: PrivateKey, seed: ByteArray): KeyPair { + // Compute HMAC(privateKey, seed). + val macBytes = deriveHMAC(privateKey, seed) + // Get the first EC curve fieldSized-bytes from macBytes. + // According to recommendations from the deterministic ECDSA rfc, see https://tools.ietf.org/html/rfc6979 + // performing a simple modular reduction would induce biases that would be detrimental to security. + // Thus, the result is not reduced modulo q and similarly to BIP32, EC curve fieldSized-bytes are utilised. + val fieldSizeMacBytes = macBytes.copyOf(parameterSpec.curve.fieldSize / 8) + + // Calculate value d for private key. + val deterministicD = BigInteger(1, fieldSizeMacBytes) + + // Key generation checks follow the BC logic found in + // https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/generators/ECKeyPairGenerator.java + // There is also an extra check to align with the BIP32 protocol, according to which + // if deterministicD >= order_of_the_curve the resulted key is invalid and we should proceed with another seed. + // TODO: We currently use SHA256(seed) when retrying, but BIP32 just skips a counter (i) that results to an invalid key. + // Although our hashing approach seems reasonable, we should check if there are alternatives, + // especially if we use counters as well. + if (deterministicD < ECConstants.TWO + || WNafUtil.getNafWeight(deterministicD) < parameterSpec.n.bitLength().ushr(2) + || deterministicD >= parameterSpec.n) { + // Instead of throwing an exception, we retry with SHA256(seed). + return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes) + } + val privateKeySpec = ECPrivateKeySpec(deterministicD, parameterSpec) + val privateKeyD = BCECPrivateKey(privateKey.algorithm, privateKeySpec, BouncyCastleProvider.CONFIGURATION) + + // Compute the public key by scalar multiplication. + // Note that BIP32 uses masterKey + mac_derived_key as the final private key and it consequently + // requires an extra point addition: master_public + mac_derived_public for the public part. + // In our model, the mac_derived_output, deterministicD, is not currently added to the masterKey and it + // it forms, by itself, the new private key, which in turn is used to compute the new public key. + val pointQ = FixedPointCombMultiplier().multiply(parameterSpec.g, deterministicD) + // This is unlikely to happen, but we should check for point at infinity. + if (pointQ.isInfinity) + // Instead of throwing an exception, we retry with SHA256(seed). + return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes) + val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec) + val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION) + + return KeyPair(publicKeyD, privateKeyD) + } + + // Deterministically generate an EdDSA key. + private fun deriveKeyPairEdDSA(privateKey: PrivateKey, seed: ByteArray): KeyPair { + // Compute HMAC(privateKey, seed). + val macBytes = deriveHMAC(privateKey, seed) + + // Calculate key pair. + val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec + val bytes = macBytes.copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length. + val privateKeyD = EdDSAPrivateKeySpec(bytes, params) + val publicKeyD = EdDSAPublicKeySpec(privateKeyD.a, params) + return KeyPair(EdDSAPublicKey(publicKeyD), EdDSAPrivateKey(privateKeyD)) + } + /** * Returns a key pair derived from the given [BigInteger] entropy. This is useful for unit tests * and other cases where you want hard-coded private keys. @@ -535,11 +677,11 @@ object Crypto { * @return a new [KeyPair] from an entropy input. * @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input. */ - fun generateKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { + fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair { when (signatureScheme) { - EDDSA_ED25519_SHA512 -> return generateEdDSAKeyPairFromEntropy(entropy) + EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy) } - throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: $signatureScheme.schemeCodeName") + throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}") } /** @@ -547,32 +689,51 @@ object Crypto { * @param entropy a [BigInteger] value. * @return a new [KeyPair] from an entropy input. */ - fun generateKeyPairFromEntropy(entropy: BigInteger): KeyPair = generateKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) + fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy) // custom key pair generator from entropy. - private fun generateEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair { + private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair { val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec - val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // need to pad the entropy to the valid seed length. + val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length. val priv = EdDSAPrivateKeySpec(bytes, params) val pub = EdDSAPublicKeySpec(priv.a, params) return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv)) } + // Compute the HMAC-SHA512 using a privateKey as the MAC_key and a seed ByteArray. + private fun deriveHMAC(privateKey: PrivateKey, seed: ByteArray): ByteArray { + // Compute hmac(privateKey, seed). + val mac = Mac.getInstance("HmacSHA512", providerMap[BouncyCastleProvider.PROVIDER_NAME]) + val keyData = when (privateKey) { + is BCECPrivateKey -> privateKey.d.toByteArray() + is EdDSAPrivateKey -> privateKey.geta() + else -> throw InvalidKeyException("Key type ${privateKey.algorithm} is not supported for deterministic key derivation") + } + val key = SecretKeySpec(keyData, "HmacSHA512") + mac.init(key) + return mac.doFinal(seed) + } + /** - * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key. + * Build a partial X.509 certificate ready for signing. + * + * @param issuer name of the issuing entity. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. */ - fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, + fun createCertificate(certificateType: CertificateType, issuer: X500Name, subject: X500Name, subjectPublicKey: PublicKey, validityWindow: Pair, - nameConstraints: NameConstraints? = null): X509Certificate { + nameConstraints: NameConstraints? = null): X509v3CertificateBuilder { - val signatureScheme = findSignatureScheme(issuerKeyPair.private) - val provider = providerMap[signatureScheme.providerName] val serial = BigInteger.valueOf(random63BitValue()) val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } }) + val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded)) val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey) - .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded))) + .addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo)) .addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA)) .addExtension(Extension.keyUsage, false, certificateType.keyUsage) .addExtension(Extension.extendedKeyUsage, false, keyPurposes) @@ -580,11 +741,52 @@ object Crypto { if (nameConstraints != null) { builder.addExtension(Extension.nameConstraints, true, nameConstraints) } + return builder + } + + /** + * Build and sign an X.509 certificate with the given signer. + * + * @param issuer name of the issuing entity. + * @param issuerSigner content signer to sign the certificate with. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + */ + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerSigner: ContentSigner, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { + val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) + return builder.build(issuerSigner).apply { + require(isValidOn(Date())) + } + } + + /** + * Build and sign an X.509 certificate with CA cert private key. + * + * @param issuer name of the issuing entity. + * @param issuerKeyPair the public & private key to sign the certificate with. + * @param subject name of the certificate subject. + * @param subjectPublicKey public key of the certificate subject. + * @param validityWindow the time period the certificate is valid for. + * @param nameConstraints any name constraints to impose on certificates signed by the generated certificate. + */ + fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair, + subject: X500Name, subjectPublicKey: PublicKey, + validityWindow: Pair, + nameConstraints: NameConstraints? = null): X509CertificateHolder { + + val signatureScheme = findSignatureScheme(issuerKeyPair.private) + val provider = providerMap[signatureScheme.providerName] + val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints) val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) - return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply { - checkValidity(Date()) - verify(issuerKeyPair.public, provider) + return builder.build(signer).apply { + require(isValidOn(Date())) + require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public))) } } @@ -617,8 +819,7 @@ object Crypto { */ @Throws(IllegalArgumentException::class) fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean { - if (!isSupportedSignatureScheme(signatureScheme)) - throw IllegalArgumentException("Unsupported signature scheme: $signatureScheme.schemeCodeName") + require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" } when (publicKey) { is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid) is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve) @@ -671,6 +872,19 @@ object Crypto { } } + /** + * Convert a public key to a supported implementation. This method is usually required to retrieve a key from an + * [X509CertificateHolder]. + * + * @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: SubjectPublicKeyInfo): PublicKey { + return Crypto.decodePublicKey(key.encoded) + } + /** * 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. 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 0090e89014..28d4e18f4a 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt @@ -2,7 +2,6 @@ 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 @@ -24,6 +23,7 @@ val NULL_PARTY = AnonymousParty(NullPublicKey) // TODO: Clean up this duplication between Null and Dummy public key @CordaSerializable +@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead") class DummyPublicKey(val s: String) : PublicKey, Comparable { override fun getAlgorithm() = "DUMMY" override fun getEncoded() = s.toByteArray() @@ -145,12 +145,12 @@ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair() * you want hard-coded private keys. * This currently works for the default signature scheme EdDSA ed25519 only. */ -fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.generateKeyPairFromEntropy(entropy) +fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEntropy(entropy) /** * Helper function for signing. * @param metaData tha attached MetaData object. - * @return a [TransactionSignature] object. + * @return a [TransactionSignature ] object. * @throws IllegalArgumentException if the signature scheme is not supported for this private key. * @throws InvalidKeyException if the private key is invalid. * @throws SignatureException if signing is not possible due to malformed data or private key. 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 138eed3c56..0c88ee2f27 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/KeyStoreUtilities.kt @@ -3,13 +3,14 @@ package net.corda.core.crypto import net.corda.core.exists import net.corda.core.read import net.corda.core.write +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.path.CertPath import java.io.IOException import java.io.InputStream import java.io.OutputStream import java.nio.file.Path import java.security.* import java.security.cert.Certificate -import java.security.cert.X509Certificate object KeyStoreUtilities { val KEYSTORE_TYPE = "JKS" @@ -67,6 +68,18 @@ 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. + * @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. + */ +fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: CertPath) { + addOrReplaceKey(alias, key, password, chain.certificates.map { it.cert }.toTypedArray()) +} + /** * Helper extension method to add, or overwrite any key data in store. * @param alias name to record the private key and certificate chain under. @@ -122,8 +135,9 @@ fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertif * @param keyPassword The password for the PrivateKey (not the store access password). */ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair { - val cert = getCertificate(alias) as X509Certificate - return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword))) + val cert = getX509Certificate(alias) + val publicKey = Crypto.toSupportedPublicKey(cert.subjectPublicKeyInfo) + return CertificateAndKeyPair(cert, KeyPair(publicKey, getSupportedKey(alias, keyPassword))) } /** @@ -131,7 +145,10 @@ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): Certi * @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 +fun KeyStore.getX509Certificate(alias: String): X509CertificateHolder { + val encoded = getCertificate(alias)?.encoded ?: throw IllegalArgumentException("No certificate under alias \"${alias}\"") + return X509CertificateHolder(encoded) +} /** * Extract a private key from a KeyStore file assuming storage alias is known. diff --git a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt index 8491208e60..babf164bc6 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/MerkleTree.kt @@ -8,7 +8,7 @@ import java.util.* * See: https://en.wikipedia.org/wiki/Merkle_tree * * Transaction is split into following blocks: inputs, attachments' refs, outputs, commands, notary, - * signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up, + * signers, tx type, time-window. Merkle Tree is kept in a recursive data structure. Building is done bottom up, * from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes. */ sealed class MerkleTree { 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 f8a3ec7c5d..79973b514f 100644 --- a/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt +++ b/core/src/main/kotlin/net/corda/core/crypto/X509Utilities.kt @@ -7,6 +7,7 @@ import org.bouncycastle.asn1.x500.X500NameBuilder import org.bouncycastle.asn1.x500.style.BCStyle import org.bouncycastle.asn1.x509.* import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemReader import java.io.FileReader @@ -24,6 +25,7 @@ import java.time.temporal.ChronoUnit import java.util.* object X509Utilities { + val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512 val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256 // Aliases for private keys and certificates. @@ -59,7 +61,7 @@ object X509Utilities { * @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 { + fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509CertificateHolder? = null): Pair { val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS) val notBefore = max(startOfDayUTC - before, parent?.notBefore) val notAfter = min(startOfDayUTC + after, parent?.notAfter) @@ -76,7 +78,7 @@ object X509Utilities { nameBuilder.addRDN(BCStyle.O, "R3") nameBuilder.addRDN(BCStyle.OU, "corda") nameBuilder.addRDN(BCStyle.L, "London") - nameBuilder.addRDN(BCStyle.C, "UK") + nameBuilder.addRDN(BCStyle.C, "GB") return nameBuilder.build() } @@ -101,10 +103,9 @@ object X509Utilities { * Create a de novo root self-signed X509 v3 CA cert. */ @JvmStatic - fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509Certificate { + fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) - val cert = Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) - return cert + return Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window) } /** @@ -119,34 +120,18 @@ object X509Utilities { */ @JvmStatic fun createCertificate(certificateType: CertificateType, - issuerCertificate: X509Certificate, issuerKeyPair: KeyPair, + issuerCertificate: X509CertificateHolder, issuerKeyPair: KeyPair, subject: X500Name, subjectPublicKey: PublicKey, validityWindow: Pair = DEFAULT_VALIDITY_WINDOW, - nameConstraints: NameConstraints? = null): X509Certificate { + nameConstraints: NameConstraints? = null): X509CertificateHolder { val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate) - val cert = Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) - return cert + return Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints) } - /** - * Build a certificate path from a trusted root certificate to a target certificate. This will always return a path - * directly from the target to the root. - * - * @param trustedRoot trusted root certificate that will be the start of the path. - * @param certificates certificates in the path. - * @param revocationEnabled whether revocation of certificates in the path should be checked. - */ - fun createCertificatePath(trustedRoot: X509Certificate, vararg certificates: X509Certificate, revocationEnabled: Boolean): CertPath { - val certFactory = CertificateFactory.getInstance("X509") - val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) - params.isRevocationEnabled = revocationEnabled - return certFactory.generateCertPath(certificates.toList()) - } - - fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) { + fun validateCertificateChain(trustedRoot: X509CertificateHolder, vararg certificates: Certificate) { require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } val certFactory = CertificateFactory.getInstance("X509") - val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null))) + val params = PKIXParameters(setOf(TrustAnchor(trustedRoot.cert, null))) params.isRevocationEnabled = false val certPath = certFactory.generateCertPath(certificates.toList()) val pathValidator = CertPathValidator.getInstance("PKIX") @@ -159,7 +144,7 @@ object X509Utilities { * @param filename Target filename. */ @JvmStatic - fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) { + fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, filename: Path) { FileWriter(filename.toFile()).use { JcaPEMWriter(it).use { it.writeObject(x509Certificate) @@ -173,11 +158,12 @@ object X509Utilities { * @return The X509Certificate that was encoded in the file. */ @JvmStatic - fun loadCertificateFromPEMFile(filename: Path): X509Certificate { + fun loadCertificateFromPEMFile(filename: Path): X509CertificateHolder { val reader = PemReader(FileReader(filename.toFile())) val pemObject = reader.readPemObject() - return CertificateStream(pemObject.content.inputStream()).nextCertificate().apply { - checkValidity() + val cert = X509CertificateHolder(pemObject.content) + return cert.apply { + isValidOn(Date()) } } @@ -218,7 +204,7 @@ object X509Utilities { CORDA_CLIENT_CA, clientKey.private, keyPass, - arrayOf(clientCACert, intermediateCACert, rootCACert)) + org.bouncycastle.cert.path.CertPath(arrayOf(clientCACert, intermediateCACert, rootCACert))) clientCAKeystore.save(clientCAKeystorePath, storePassword) val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeyStorePath, storePassword) @@ -226,7 +212,7 @@ object X509Utilities { CORDA_CLIENT_TLS, tlsKey.private, keyPass, - arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)) + org.bouncycastle.cert.path.CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))) tlsKeystore.save(sslKeyStorePath, storePassword) } @@ -275,7 +261,9 @@ 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() +val X500Name.locationOrNull: String? get() = try { location } catch (e: Exception) { null } val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject +val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this) class CertificateStream(val input: InputStream) { private val certificateFactory = CertificateFactory.getInstance("X.509") @@ -283,12 +271,13 @@ class CertificateStream(val input: InputStream) { fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate } -data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair) +data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair) enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) { ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true), TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false), - IDENTITY(KeyUsage(KeyUsage.digitalSignature), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false) -} \ No newline at end of file + // TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints + IDENTITY(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true) +} diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt index d1114d0946..153baeae46 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowException.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowException.kt @@ -1,7 +1,9 @@ package net.corda.core.flows -import net.corda.core.serialization.CordaSerializable +import net.corda.core.utilities.CordaException +import net.corda.core.utilities.CordaRuntimeException +// DOCSTART 1 /** * Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end. * The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a @@ -11,17 +13,18 @@ import net.corda.core.serialization.CordaSerializable * [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service. * It is recommended a [FlowLogic] document the [FlowException] types it can throw. */ -@CordaSerializable -open class FlowException(override val message: String?, override val cause: Throwable?) : Exception() { +open class FlowException(message: String?, cause: Throwable?) : CordaException(message, cause) { constructor(message: String?) : this(message, null) constructor(cause: Throwable?) : this(cause?.toString(), cause) constructor() : this(null, null) } +// DOCEND 1 /** * Thrown when a flow session ends unexpectedly due to a type mismatch (the other side sent an object of a type * that we were not expecting), or the other side had an internal error, or the other side terminated when we * were waiting for a response. */ -@CordaSerializable -class FlowSessionException(message: String) : RuntimeException(message) +class FlowSessionException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { + constructor(msg: String) : this(msg, null) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt new file mode 100644 index 0000000000..3860e73a06 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt @@ -0,0 +1,31 @@ +package net.corda.core.flows + +import net.corda.core.contracts.ScheduledStateRef +import net.corda.core.identity.Party +import net.corda.core.serialization.CordaSerializable +import java.security.Principal + +/** + * FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC], + * communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled] + * or via the Corda Shell [FlowInitiator.Shell]. + */ +@CordaSerializable +sealed class FlowInitiator : Principal { + /** Started using [net.corda.core.messaging.CordaRPCOps.startFlowDynamic]. */ + data class RPC(val username: String) : FlowInitiator() { + override fun getName(): String = username + } + /** Started when we get new session initiation request. */ + data class Peer(val party: Party) : FlowInitiator() { + override fun getName(): String = party.name.toString() + } + /** Started as scheduled activity. */ + data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() { + override fun getName(): String = "Scheduler" + } + // TODO When proper ssh access enabled, add username/use RPC? + object Shell : FlowInitiator() { + override fun getName(): String = "Shell User" + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt index 0530a4fa6b..a73cda8427 100644 --- a/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt +++ b/core/src/main/kotlin/net/corda/core/flows/FlowLogic.kt @@ -3,6 +3,7 @@ package net.corda.core.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.crypto.SecureHash import net.corda.core.identity.Party +import net.corda.core.internal.FlowStateMachine import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker diff --git a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt deleted file mode 100644 index b4d2de2817..0000000000 --- a/core/src/main/kotlin/net/corda/core/flows/FlowStateMachine.kt +++ /dev/null @@ -1,81 +0,0 @@ -package net.corda.core.flows - -import co.paralleluniverse.fibers.Suspendable -import com.google.common.util.concurrent.ListenableFuture -import net.corda.core.contracts.ScheduledStateRef -import net.corda.core.crypto.SecureHash -import net.corda.core.identity.Party -import net.corda.core.node.ServiceHub -import net.corda.core.serialization.CordaSerializable -import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.UntrustworthyData -import org.slf4j.Logger -import java.security.Principal -import java.util.* - -/** - * FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC], - * communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled] - * or via the Corda Shell [FlowInitiator.Shell]. - */ -@CordaSerializable -sealed class FlowInitiator : Principal { - /** Started using [net.corda.core.messaging.CordaRPCOps.startFlowDynamic]. */ - data class RPC(val username: String) : FlowInitiator() { - override fun getName(): String = username - } - /** Started when we get new session initiation request. */ - data class Peer(val party: Party) : FlowInitiator() { - override fun getName(): String = party.name.toString() - } - /** Started as scheduled activity. */ - data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() { - override fun getName(): String = "Scheduler" - } - // TODO When proper ssh access enabled, add username/use RPC? - object Shell : FlowInitiator() { - override fun getName(): String = "Shell User" - } -} - -/** - * A unique identifier for a single state machine run, valid across node restarts. Note that a single run always - * has at least one flow, but that flow may also invoke sub-flows: they all share the same run id. - */ -@CordaSerializable -data class StateMachineRunId(val uuid: UUID) { - companion object { - fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID()) - } - - override fun toString(): String = "[$uuid]" -} - -/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ -interface FlowStateMachine { - @Suspendable - fun sendAndReceive(receiveType: Class, - otherParty: Party, - payload: Any, - sessionFlow: FlowLogic<*>, - retrySend: Boolean = false): UntrustworthyData - - @Suspendable - fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData - - @Suspendable - fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) - - @Suspendable - fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction - - fun checkFlowPermission(permissionName: String, extraAuditData: Map) - - fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) - - val serviceHub: ServiceHub - val logger: Logger - val id: StateMachineRunId - val resultFuture: ListenableFuture - val flowInitiator: FlowInitiator -} diff --git a/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt new file mode 100644 index 0000000000..25f2433ea0 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt @@ -0,0 +1,16 @@ +package net.corda.core.flows + +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.reflect.KClass + +/** + * This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that + * does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow]. + * + * The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating + * to initiated flow mapping. + * + * @see InitiatingFlow + */ +@Target(CLASS) +annotation class InitiatedBy(val value: KClass>) \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/flows/InitiatingFlow.kt b/core/src/main/kotlin/net/corda/core/flows/InitiatingFlow.kt index 75ead4b8e1..e27cd5a23b 100644 --- a/core/src/main/kotlin/net/corda/core/flows/InitiatingFlow.kt +++ b/core/src/main/kotlin/net/corda/core/flows/InitiatingFlow.kt @@ -5,8 +5,7 @@ import kotlin.annotation.AnnotationTarget.CLASS /** * This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty - * and request they start their side of the flow communication. To ensure that this is correctly applied - * [net.corda.core.node.PluginServiceHub.registerServiceFlow] checks the initiating flow class has this annotation. + * and request they start their side of the flow communication. * * There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This * integer value should be incremented whenever there is a release of this flow which has changes that are not backwards @@ -19,6 +18,11 @@ import kotlin.annotation.AnnotationTarget.CLASS * * The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version * number can change independently of the platform version. + * + * If you are customising an existing initiating flow by sub-classing it then there's no need to specify this annotation + * again. In fact doing so is an error and checks are made to make sure this doesn't occur. + * + * @see InitiatedBy */ // TODO Add support for multiple versions once CorDapps are loaded in separate class loaders @Target(CLASS) diff --git a/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt index 6eafd3d699..c1724d615d 100644 --- a/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt +++ b/core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt @@ -1,6 +1,5 @@ package net.corda.core.flows -import java.lang.annotation.Inherited import kotlin.annotation.AnnotationTarget.CLASS /** @@ -9,7 +8,6 @@ import kotlin.annotation.AnnotationTarget.CLASS * 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/flows/StateMachineRunId.kt b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt new file mode 100644 index 0000000000..30f0d9599a --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/flows/StateMachineRunId.kt @@ -0,0 +1,17 @@ +package net.corda.core.flows + +import net.corda.core.serialization.CordaSerializable +import java.util.* + +/** + * A unique identifier for a single state machine run, valid across node restarts. Note that a single run always + * has at least one flow, but that flow may also invoke sub-flows: they all share the same run id. + */ +@CordaSerializable +data class StateMachineRunId(val uuid: UUID) { + companion object { + fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID()) + } + + override fun toString(): String = "[$uuid]" +} \ 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 6b285c3698..5c81e0c4b2 100644 --- a/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt +++ b/core/src/main/kotlin/net/corda/core/identity/AbstractParty.kt @@ -18,6 +18,15 @@ abstract class AbstractParty(val owningKey: PublicKey) { override fun hashCode(): Int = owningKey.hashCode() abstract fun nameOrNull(): X500Name? + /** + * Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal + * ledger. + */ abstract fun ref(bytes: OpaqueBytes): PartyAndReference + + /** + * Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal + * ledger. + */ fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes)) } \ 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 b94f266b8c..7bc6ebab8b 100644 --- a/core/src/main/kotlin/net/corda/core/identity/Party.kt +++ b/core/src/main/kotlin/net/corda/core/identity/Party.kt @@ -3,6 +3,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.CordaSerializable import net.corda.core.serialization.OpaqueBytes import org.bouncycastle.asn1.x500.X500Name import java.security.PublicKey @@ -14,7 +15,7 @@ import java.security.PublicKey * cryptographic public key primitives into a tree structure. * * For example: Alice has two key pairs (pub1/priv1 and pub2/priv2), and wants to be able to sign transactions with either of them. - * Her advertised [Party] then has a legal X.500 [name] "CN=Alice Corp,O=Alice Corp,L=London,C=UK" and an [owningKey] + * Her advertised [Party] then has a legal X.500 [name] "CN=Alice Corp,O=Alice Corp,L=London,C=GB" and an [owningKey] * "pub1 or pub2". * * [Party] is also used for service identities. E.g. Alice may also be running an interest rate oracle on her Corda node, @@ -27,7 +28,7 @@ import java.security.PublicKey * @see CompositeKey */ class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { - constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public) + constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public) override fun toString() = name.toString() override fun nameOrNull(): X500Name? = name diff --git a/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt new file mode 100644 index 0000000000..12557d562e --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/identity/PartyAndCertificate.kt @@ -0,0 +1,33 @@ +package net.corda.core.identity + +import net.corda.core.serialization.CordaSerializable +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder +import java.security.PublicKey +import java.security.cert.CertPath + +/** + * A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of + * [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party, + * not part of the identifier themselves. + */ +@CordaSerializable +data class PartyAndCertificate(val party: Party, + val certificate: X509CertificateHolder, + val certPath: CertPath) { + constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath) + val name: X500Name + get() = party.name + val owningKey: PublicKey + get() = party.owningKey + + override fun equals(other: Any?): Boolean { + return if (other is PartyAndCertificate) + party == other.party + else + false + } + + override fun hashCode(): Int = party.hashCode() + override fun toString(): String = party.toString() +} diff --git a/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt new file mode 100644 index 0000000000..8c87cfaa51 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/internal/FlowStateMachine.kt @@ -0,0 +1,42 @@ +package net.corda.core.internal + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.util.concurrent.ListenableFuture +import net.corda.core.crypto.SecureHash +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StateMachineRunId +import net.corda.core.identity.Party +import net.corda.core.node.ServiceHub +import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.UntrustworthyData +import org.slf4j.Logger + +/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */ +interface FlowStateMachine { + @Suspendable + fun sendAndReceive(receiveType: Class, + otherParty: Party, + payload: Any, + sessionFlow: FlowLogic<*>, + retrySend: Boolean = false): UntrustworthyData + + @Suspendable + fun receive(receiveType: Class, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData + + @Suspendable + fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>) + + @Suspendable + fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction + + fun checkFlowPermission(permissionName: String, extraAuditData: Map) + + fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map) + + val serviceHub: ServiceHub + val logger: Logger + val id: StateMachineRunId + val resultFuture: ListenableFuture + val flowInitiator: FlowInitiator +} 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 190f2456a0..ebdb291174 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/CordaRPCOps.kt @@ -244,6 +244,16 @@ interface CordaRPCOps : RPCOps { */ fun partyFromX500Name(x500Name: X500Name): Party? + /** + * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may + * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results + * but rather show them via a user interface and let the user pick the one they wanted. + * + * @param query The string to check against the X.500 name components + * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. + */ + fun partiesFromName(query: String, exactMatch: Boolean): Set + /** Enumerates the class names of the flows that this node knows about. */ fun registeredFlows(): List } diff --git a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt index 6854d506de..cf10588939 100644 --- a/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt +++ b/core/src/main/kotlin/net/corda/core/messaging/FlowHandle.kt @@ -40,8 +40,8 @@ interface FlowProgressHandle : FlowHandle { @CordaSerializable data class FlowHandleImpl( - override val id: StateMachineRunId, - override val returnValue: ListenableFuture) : FlowHandle { + override val id: StateMachineRunId, + override val returnValue: ListenableFuture) : FlowHandle { // Remember to add @Throws to FlowHandle.close() if this throws an exception. override fun close() { @@ -51,9 +51,9 @@ data class FlowHandleImpl( @CordaSerializable data class FlowProgressHandleImpl( - override val id: StateMachineRunId, - override val returnValue: ListenableFuture, - override val progress: Observable) : FlowProgressHandle { + override val id: StateMachineRunId, + override val returnValue: ListenableFuture, + override val progress: Observable) : FlowProgressHandle { // Remember to add @Throws to FlowProgressHandle.close() if this throws an exception. override fun close() { 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 6d73439665..14835e1e1f 100644 --- a/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt +++ b/core/src/main/kotlin/net/corda/core/node/CordaPluginRegistry.kt @@ -9,17 +9,16 @@ import java.util.function.Function * 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. - */ + + @Suppress("unused") + @Deprecated("This is no longer in use, moved to WebServerPluginRegistry class in webserver module", + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("net.corda.webserver.services.WebServerPluginRegistry")) 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() - */ + + @Suppress("unused") + @Deprecated("This is no longer in use, moved to WebServerPluginRegistry class in webserver module", + level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("net.corda.webserver.services.WebServerPluginRegistry")) open val staticServeDirs: Map get() = emptyMap() @Suppress("unused") @@ -33,6 +32,9 @@ abstract class CordaPluginRegistry { * 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. */ + @Suppress("unused") + @Deprecated("This is no longer used. If you need to create your own service, such as an oracle, then use the " + + "@CordaService annotation. For flow registrations use @InitiatedBy.", level = DeprecationLevel.ERROR) open val servicePlugins: List> get() = emptyList() /** diff --git a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt index e7c542cdca..15f98be512 100644 --- a/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/NodeInfo.kt @@ -1,31 +1,41 @@ package net.corda.core.node import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable +import org.bouncycastle.cert.X509CertificateHolder /** * Information for an advertised service including the service specific identity information. * The identity can be used in flows and is distinct from the Node's legalIdentity */ @CordaSerializable -data class ServiceEntry(val info: ServiceInfo, val identity: Party) +data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate) /** * Info about a network node that acts on behalf of some form of contract party. */ @CordaSerializable data class NodeInfo(val address: SingleMessageRecipient, - val legalIdentity: Party, + val legalIdentityAndCert: PartyAndCertificate, val platformVersion: Int, var advertisedServices: List = emptyList(), val physicalLocation: PhysicalLocation? = null) { init { - require(advertisedServices.none { it.identity == legalIdentity }) { "Service identities must be different from node legal identity" } + require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" } } - val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity - fun serviceIdentities(type: ServiceType): List = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity } + val legalIdentity: Party + get() = legalIdentityAndCert.party + val notaryIdentity: Party + get() = advertisedServices.single { it.info.type.isNotary() }.identity.party + fun serviceIdentities(type: ServiceType): List { + return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } + } + fun servideIdentitiesAndCert(type: ServiceType): List { + return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity } + } } diff --git a/core/src/main/kotlin/net/corda/core/node/PluginServiceHub.kt b/core/src/main/kotlin/net/corda/core/node/PluginServiceHub.kt index b6ead1d051..be193d2dfe 100644 --- a/core/src/main/kotlin/net/corda/core/node/PluginServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/PluginServiceHub.kt @@ -7,22 +7,7 @@ import net.corda.core.identity.Party * A service hub to be used by the [CordaPluginRegistry] */ interface PluginServiceHub : ServiceHub { - /** - * Register the service flow factory to use when an initiating party attempts to communicate with us. The registration - * is done against the [Class] object of the client flow to the service flow. What this means is if a counterparty - * starts a [FlowLogic] represented by [initiatingFlowClass] and starts communication with us, we will execute the service - * flow produced by [serviceFlowFactory]. This service flow has respond correctly to the sends and receives the client - * does. - * @param initiatingFlowClass [Class] of the client flow involved in this client-server communication. - * @param serviceFlowFactory Lambda which produces a new service flow for each new client flow communication. The - * [Party] parameter of the factory is the client's identity. - * @throws IllegalArgumentException If [initiatingFlowClass] is not annotated with [net.corda.core.flows.InitiatingFlow]. - */ - fun registerServiceFlow(initiatingFlowClass: Class>, serviceFlowFactory: (Party) -> FlowLogic<*>) - - @Suppress("UNCHECKED_CAST") - @Deprecated("This is scheduled to be removed in a future release", ReplaceWith("registerServiceFlow")) - fun registerFlowInitiator(markerClass: Class<*>, flowFactory: (Party) -> FlowLogic<*>) { - registerServiceFlow(markerClass as Class>, flowFactory) - } + @Deprecated("This is no longer used. Instead annotate the flows produced by your factory with @InitiatedBy and have " + + "them point to the initiating flow class.", level = DeprecationLevel.ERROR) + fun registerFlowInitiator(initiatingFlowClass: Class>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit } 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 5e589834dd..0c0af907f8 100644 --- a/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt +++ b/core/src/main/kotlin/net/corda/core/node/ServiceHub.kt @@ -3,6 +3,7 @@ package net.corda.core.node import net.corda.core.contracts.* import net.corda.core.crypto.DigitalSignature import net.corda.core.node.services.* +import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -44,6 +45,13 @@ interface ServiceHub : ServicesForResolution { val clock: Clock val myInfo: NodeInfo + /** + * Return the singleton instance of the given Corda service type. This is a class that is annotated with + * [CordaService] and will have automatically been registered by the node. + * @throws IllegalArgumentException If [type] is not annotated with [CordaService] or if the instance is not found. + */ + fun cordaService(type: Class): T + /** * Given a [SignedTransaction], writes it to the local storage for validated transactions and then * sends them to the vault for further processing. Expects to be run within a database transaction. @@ -141,7 +149,7 @@ interface ServiceHub : ServicesForResolution { * @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 { + fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable): SignedTransaction { var stx: SignedTransaction? = null for (pubKey in signingPubKeys) { stx = if (stx == null) { diff --git a/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt new file mode 100644 index 0000000000..658dafa9a2 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/CordaService.kt @@ -0,0 +1,22 @@ +package net.corda.core.node.services + +import kotlin.annotation.AnnotationTarget.CLASS + +/** + * Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation. + * Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This + * construtor will be invoked during node start to initialise the service. The service hub provided can be used to get + * information about the node that may be necessary for the service. Corda services are created as singletons within + * the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService]. + * + * The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows. + * (If possible extend [net.corda.core.serialization.SingletonSerializeAsToken] instead as it removes the boilerplate.) + * + * The annotated class should expose its [ServiceType] via a public static field named `type`, so that the service is + * only loaded in nodes that declare the type in their advertisedServices. + */ +// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken +// TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and +// the need for the service type (which can be exposed by a simple getter) +@Target(CLASS) +annotation class CordaService 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 4dafa52ef0..ce5d5f9262 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,35 +1,41 @@ 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 net.corda.core.identity.* +import net.corda.core.node.NodeInfo import org.bouncycastle.asn1.x500.X500Name +import java.security.InvalidAlgorithmParameterException import java.security.PublicKey import java.security.cert.CertPath -import java.security.cert.X509Certificate +import java.security.cert.CertificateExpiredException +import java.security.cert.CertificateNotYetValidException /** - * An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports - * lookup of a party given its key. This is obviously very incomplete and does not reflect everything a real identity - * service would provide. + * An identity service maintains a directory of parties by their associated distinguished name/public keys and thus + * supports lookup of a party given its key, or name. The service also manages the certificates linking confidential + * identities back to the well known identity (i.e. the identity in the network map) of a party. */ interface IdentityService { - fun registerIdentity(party: Party) + /** + * Verify and then store a well known identity. + * + * @param party a party representing a legal entity. + * @throws IllegalArgumentException if the certificate path is invalid, or if there is already an existing + * certificate chain for the anonymous party. + */ + @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) + fun registerIdentity(party: PartyAndCertificate) /** - * Verify and then store the certificates proving that an anonymous party's key is owned by the given full - * party. + * Verify and then store an identity. * - * @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. + * @param anonymousParty a party representing a legal entity in a transaction. + * @param path certificate path from the trusted root to the party. + * @throws IllegalArgumentException if the certificate path is invalid, or if there is already an existing + * certificate chain for the anonymous party. */ - // TODO: Move this into internal identity service once available - @Throws(IllegalArgumentException::class) - fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath) + @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) + fun registerAnonymousIdentity(anonymousParty: AnonymousParty, party: Party, path: CertPath) /** * Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with @@ -44,24 +50,63 @@ interface IdentityService { * Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be * used in preference where possible. */ - fun getAllIdentities(): Iterable + fun getAllIdentities(): Iterable + + /** + * Get the certificate and path for a well known identity. + * + * @return the party and certificate, or null if unknown. + */ + fun certificateFromParty(party: Party): PartyAndCertificate? // There is no method for removing identities, as once we are made aware of a Party we want to keep track of them // indefinitely. It may be that in the long term we need to drop or archive very old Party information for space, // but for now this is not supported. fun partyFromKey(key: PublicKey): Party? - @Deprecated("Use partyFromX500Name") + @Deprecated("Use partyFromX500Name or partiesFromName") fun partyFromName(name: String): Party? fun partyFromX500Name(principal: X500Name): Party? + /** + * Resolve the well known identity of a party. If the party passed in is already a well known identity + * (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity, or null if unknown. + */ fun partyFromAnonymous(party: AbstractParty): Party? + + /** + * Resolve the well known identity of a party. If the party passed in is already a well known identity + * (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity, or null if unknown. + */ fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + /** + * Resolve the well known identity of a party. Throws an exception if the party cannot be identified. + * If the party passed in is already a well known identity (i.e. a [Party]) this returns it as-is. + * + * @return the well known identity. + * @throws IllegalArgumentException + */ + fun requirePartyFromAnonymous(party: AbstractParty): Party + /** * Get the certificate chain showing an anonymous party is owned by the given party. */ fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath? + /** + * Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may + * get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results + * but rather show them via a user interface and let the user pick the one they wanted. + * + * @param query The string to check against the X.500 name components + * @param exactMatch If true, a case sensitive match is done against each component of each X.500 name. + */ + fun partiesFromName(query: String, exactMatch: Boolean): Set + class UnknownAnonymousPartyException(msg: String) : Exception(msg) } diff --git a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt index e7ec60dd6f..94ff9c94c5 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/PartyInfo.kt @@ -1,6 +1,7 @@ package net.corda.core.node.services import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceEntry @@ -8,10 +9,10 @@ import net.corda.core.node.ServiceEntry * Holds information about a [Party], which may refer to either a specific node or a service. */ sealed class PartyInfo { - abstract val party: Party + abstract val party: PartyAndCertificate data class Node(val node: NodeInfo) : PartyInfo() { - override val party get() = node.legalIdentity + override val party get() = node.legalIdentityAndCert } data class Service(val service: ServiceEntry) : PartyInfo() { 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 6116441b0a..7899a50094 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,11 +3,14 @@ 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.CompositeKey import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.keys import net.corda.core.flows.FlowException import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.Sort @@ -17,9 +20,12 @@ import net.corda.core.toFuture import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.WireTransaction +import org.bouncycastle.cert.X509CertificateHolder import rx.Observable import java.io.InputStream import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.X509Certificate import java.time.Instant import java.util.* @@ -357,12 +363,6 @@ inline fun VaultService.linearHeadsOfType() = states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED)) .associateBy { it.state.data.linearId }.mapValues { it.value } -// TODO: Remove this from the interface -// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf()))")) -inline fun VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType().values.filter { - it.state.data.parties.any { it == party } -} - class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) { override fun toString() = "Soft locking error: $message" } @@ -385,6 +385,17 @@ interface KeyManagementService { @Suspendable fun freshKey(): PublicKey + /** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding + * [X509Certificate] and adds it to the identity service. + * + * @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for. + * @param revocationEnabled whether to check revocation status of certificates in the certificate path. + * @return X.509 certificate and path to the trust root. + */ + @Suspendable + fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair + /** 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, @@ -398,10 +409,10 @@ interface KeyManagementService { fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey } -// TODO: Move to a more appropriate location /** * An interface that denotes a service that can accept file uploads. */ +// TODO This is no longer used and can be removed interface FileUploader { /** * Accepts the data in the given input stream, and returns some sort of useful return message that will be sent diff --git a/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt new file mode 100644 index 0000000000..b06ee522af --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/node/services/TimeWindowChecker.kt @@ -0,0 +1,26 @@ +package net.corda.core.node.services + +import net.corda.core.contracts.TimeWindow +import net.corda.core.seconds +import net.corda.core.until +import java.time.Clock +import java.time.Duration + +/** + * Checks if the given time-window falls within the allowed tolerance interval. + */ +class TimeWindowChecker(val clock: Clock = Clock.systemUTC(), + val tolerance: Duration = 30.seconds) { + fun isValid(timeWindow: TimeWindow): Boolean { + val untilTime = timeWindow.untilTime + val fromTime = timeWindow.fromTime + + val now = clock.instant() + + // We don't need to test for (fromTime == null && untilTime == null) or backwards bounds because the TimeWindow + // constructor already checks that. + if (untilTime != null && untilTime until now > tolerance) return false + if (fromTime != null && now until fromTime > tolerance) return false + return true + } +} diff --git a/core/src/main/kotlin/net/corda/core/node/services/TimestampChecker.kt b/core/src/main/kotlin/net/corda/core/node/services/TimestampChecker.kt deleted file mode 100644 index 2948aa92ef..0000000000 --- a/core/src/main/kotlin/net/corda/core/node/services/TimestampChecker.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.contracts.Timestamp -import net.corda.core.seconds -import net.corda.core.until -import java.time.Clock -import java.time.Duration - -/** - * Checks if the given timestamp falls within the allowed tolerance interval. - */ -class TimestampChecker(val clock: Clock = Clock.systemUTC(), - val tolerance: Duration = 30.seconds) { - fun isValid(timestampCommand: Timestamp): Boolean { - val before = timestampCommand.before - val after = timestampCommand.after - - val now = clock.instant() - - // We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand - // constructor already checks that. - if (before != null && before until now > tolerance) return false - if (after != null && now until after > tolerance) return false - return true - } -} diff --git a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt index 9c8b058f8a..815b5e2465 100644 --- a/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt +++ b/core/src/main/kotlin/net/corda/core/node/services/vault/QueryCriteria.kt @@ -1,6 +1,5 @@ package net.corda.core.node.services.vault -import net.corda.core.contracts.Commodity import net.corda.core.contracts.ContractState import net.corda.core.contracts.StateRef import net.corda.core.contracts.UniqueIdentifier diff --git a/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt b/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt new file mode 100644 index 0000000000..6f3bb8d3dd --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/AllButBlacklisted.kt @@ -0,0 +1,125 @@ +package net.corda.core.serialization + +import sun.misc.Unsafe +import sun.security.util.Password +import java.io.* +import java.lang.invoke.* +import java.lang.reflect.* +import java.net.* +import java.security.* +import java.sql.Connection +import java.util.* +import java.util.logging.Handler +import java.util.zip.ZipFile +import kotlin.collections.HashSet +import kotlin.collections.LinkedHashSet + +/** + * This is a [ClassWhitelist] implementation where everything is whitelisted except for blacklisted classes and interfaces. + * In practice, as flows are arbitrary code in which it is convenient to do many things, + * we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint. + * Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour. + * Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well. + */ +object AllButBlacklisted : ClassWhitelist { + + private val blacklistedClasses = hashSetOf( + + // Known blacklisted classes. + Thread::class.java.name, + HashSet::class.java.name, + HashMap::class.java.name, + ClassLoader::class.java.name, + Handler::class.java.name, // MemoryHandler, StreamHandler + Runtime::class.java.name, + Unsafe::class.java.name, + ZipFile::class.java.name, + Provider::class.java.name, + SecurityManager::class.java.name, + Random::class.java.name, + + // Known blacklisted interfaces. + Connection::class.java.name, + // TODO: AutoCloseable::class.java.name, + + // java.security. + KeyStore::class.java.name, + Password::class.java.name, + AccessController::class.java.name, + Permission::class.java.name, + + // java.net. + DatagramSocket::class.java.name, + ServerSocket::class.java.name, + Socket::class.java.name, + URLConnection::class.java.name, + // TODO: add more from java.net. + + // java.io. + Console::class.java.name, + File::class.java.name, + FileDescriptor::class.java.name, + FilePermission::class.java.name, + RandomAccessFile::class.java.name, + Reader::class.java.name, + Writer::class.java.name, + // TODO: add more from java.io. + + // java.lang.invoke classes. + CallSite::class.java.name, // for all CallSites eg MutableCallSite, VolatileCallSite etc. + LambdaMetafactory::class.java.name, + MethodHandle::class.java.name, + MethodHandleProxies::class.java.name, + MethodHandles::class.java.name, + MethodHandles.Lookup::class.java.name, + MethodType::class.java.name, + SerializedLambda::class.java.name, + SwitchPoint::class.java.name, + + // java.lang.invoke interfaces. + MethodHandleInfo::class.java.name, + + // java.lang.invoke exceptions. + LambdaConversionException::class.java.name, + WrongMethodTypeException::class.java.name, + + // java.lang.reflect. + AccessibleObject::class.java.name, // For Executable, Field, Method, Constructor. + Modifier::class.java.name, + Parameter::class.java.name, + ReflectPermission::class.java.name + // TODO: add more from java.lang.reflect. + ) + + // Specifically exclude classes from the blacklist, + // even if any of their superclasses and/or implemented interfaces are blacklisted. + private val forciblyAllowedClasses = hashSetOf( + LinkedHashSet::class.java.name, + LinkedHashMap::class.java.name, + InputStream::class.java.name, + BufferedInputStream::class.java.name, + Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream").name + ) + + /** + * This implementation supports inheritance; thus, if a superclass or superinterface is blacklisted, so is the input class. + */ + override fun hasListed(type: Class<*>): Boolean { + // Check if excluded. + if (type.name !in forciblyAllowedClasses) { + // Check if listed. + if (type.name in blacklistedClasses) + throw IllegalStateException("Class ${type.name} is blacklisted, so it cannot be used in serialization.") + // Inheritance check. + else { + val aMatch = blacklistedClasses.firstOrNull { Class.forName(it).isAssignableFrom(type) } + if (aMatch != null) { + // TODO: blacklistedClasses += type.name // add it, so checking is faster next time we encounter this class. + val matchType = if (Class.forName(aMatch).isInterface) "superinterface" else "superclass" + throw IllegalStateException("The $matchType $aMatch of ${type.name} is blacklisted, so it cannot be used in serialization.") + } + } + } + return true + } +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt index 821b7e784f..d9755cec29 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/CordaClassResolver.kt @@ -25,6 +25,10 @@ fun makeNoWhitelistClassResolver(): ClassResolver { return CordaClassResolver(AllWhitelist) } +fun makeAllButBlacklistedClassResolver(): ClassResolver { + return CordaClassResolver(AllButBlacklisted) +} + class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() { /** Returns the registration for the specified class, or null if the class is not registered. */ override fun getRegistration(type: Class<*>): Registration? { @@ -55,8 +59,9 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() return checkClass(type.superclass) } // It's safe to have the Class already, since Kryo loads it with initialisation off. - val hasAnnotation = checkForAnnotation(type) - if (!hasAnnotation && !whitelist.hasListed(type)) { + // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw a NotSerializableException if input class is blacklisted. + // Thus, blacklisting precedes annotation checking. + if (!whitelist.hasListed(type) && !checkForAnnotation(type)) { throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization") } return null 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 825ce18a20..a88e2ec073 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/DefaultKryoCustomizer.kt @@ -18,6 +18,7 @@ import net.corda.core.utilities.NonEmptySetSerializer import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey @@ -102,11 +103,8 @@ object DefaultKryoCustomizer { 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(X509CertificateHolder::class.java, X509CertificateSerializer) register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPublicKey::class.java, PublicKeySerializer) 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 c18bfedc0b..65de2949c5 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/Kryo.kt @@ -21,6 +21,7 @@ import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.bouncycastle.asn1.ASN1InputStream import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.ByteArrayInputStream @@ -33,7 +34,6 @@ 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.* @@ -329,7 +329,7 @@ object WireTransactionSerializer : Serializer() { kryo.writeClassAndObject(output, obj.notary) kryo.writeClassAndObject(output, obj.mustSign) kryo.writeClassAndObject(output, obj.type) - kryo.writeClassAndObject(output, obj.timestamp) + kryo.writeClassAndObject(output, obj.timeWindow) } private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List): ClassLoader? { @@ -357,8 +357,8 @@ object WireTransactionSerializer : Serializer() { val notary = kryo.readClassAndObject(input) as Party? val signers = kryo.readClassAndObject(input) as List val transactionType = kryo.readClassAndObject(input) as TransactionType - val timestamp = kryo.readClassAndObject(input) as Timestamp? - return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timestamp) + val timeWindow = kryo.readClassAndObject(input) as TimeWindow? + return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timeWindow) } } } @@ -463,7 +463,7 @@ object KotlinObjectSerializer : Serializer() { } // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. -private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeNoWhitelistClassResolver())) }.build() +private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build() private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build() // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. @@ -636,16 +636,15 @@ object CertPathSerializer : Serializer() { } /** - * For serialising an [CX509Certificate] in an X.500 standard format. + * For serialising an [CX509CertificateHolder] 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 +object X509CertificateSerializer : Serializer() { + override fun read(kryo: Kryo, input: Input, type: Class): X509CertificateHolder { + return X509CertificateHolder(input.readBytes()) } - override fun write(kryo: Kryo, output: Output, obj: X509Certificate) { + override fun write(kryo: Kryo, output: Output, obj: X509CertificateHolder) { output.writeBytes(obj.encoded) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt index 2935b19cb9..40f586a88e 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPPrimitiveSerializer.kt @@ -7,7 +7,7 @@ import java.lang.reflect.Type /** * Serializer / deserializer for native AMQP types (Int, Float, String etc). */ -class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { +class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { override val typeDescriptor: String = SerializerFactory.primitiveTypeName(Primitives.wrap(clazz))!! override val type: Type = clazz @@ -19,5 +19,5 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer { data.putObject(obj) } - override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any = obj + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = obj } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPSerializer.kt index 20465bb9cb..b2917c39cd 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/AMQPSerializer.kt @@ -6,7 +6,7 @@ import java.lang.reflect.Type /** * Implemented to serialize and deserialize different types of objects to/from AMQP. */ -interface AMQPSerializer { +interface AMQPSerializer { /** * The JVM type this can serialize and deserialize. */ @@ -34,5 +34,5 @@ interface AMQPSerializer { /** * Read the given object from the input. The envelope is provided in case the schema is required. */ - fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any + fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt index 2b1c6f5c55..0cf705e16d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ArraySerializer.kt @@ -9,14 +9,12 @@ import java.lang.reflect.Type /** * Serialization / deserialization of arrays. */ -class ArraySerializer(override val type: Type) : AMQPSerializer { - private val typeName = type.typeName +class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer { + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}" + internal val elementType: Type = makeElementType() - private val elementType: Type = makeElementType() - - private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(type.typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) private fun makeElementType(): Type { return (type as? Class<*>)?.componentType ?: (type as GenericArrayType).genericComponentType @@ -39,8 +37,10 @@ class ArraySerializer(override val type: Type) : AMQPSerializer { } } - override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any { - return (obj as List<*>).map { input.readObjectOrNull(it, envelope, elementType) }.toArrayOfType(elementType) + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { + if (obj is List<*>) { + return obj.map { input.readObjectOrNull(it, schema, elementType) }.toArrayOfType(elementType) + } else throw NotSerializableException("Expected a List but found $obj") } private fun List.toArrayOfType(type: Type): Any { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt index 3e2d74002c..0f4421de6c 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CollectionSerializer.kt @@ -12,28 +12,27 @@ import kotlin.collections.Set /** * Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s. */ -class CollectionSerializer(val declaredType: ParameterizedType) : AMQPSerializer { +class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString()) - private val typeName = declaredType.toString() - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}" + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" companion object { - private val supportedTypes: Map>, (Collection<*>) -> Collection<*>> = mapOf( - Collection::class.java to { coll -> coll }, - List::class.java to { coll -> coll }, - Set::class.java to { coll -> Collections.unmodifiableSet(LinkedHashSet(coll)) }, - SortedSet::class.java to { coll -> Collections.unmodifiableSortedSet(TreeSet(coll)) }, - NavigableSet::class.java to { coll -> Collections.unmodifiableNavigableSet(TreeSet(coll)) } + private val supportedTypes: Map>, (List<*>) -> Collection<*>> = mapOf( + Collection::class.java to { list -> Collections.unmodifiableCollection(list) }, + List::class.java to { list -> Collections.unmodifiableList(list) }, + Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) }, + SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) }, + NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) } ) + + private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> { + return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.") + } } - private val concreteBuilder: (Collection<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) + private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) - private fun findConcreteType(clazz: Class<*>): (Collection<*>) -> Collection<*> { - return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") - } - - private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { @@ -52,8 +51,8 @@ class CollectionSerializer(val declaredType: ParameterizedType) : AMQPSerializer } } - override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { // TODO: Can we verify the entries in the list? - return concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, envelope, declaredType.actualTypeArguments[0]) }) + return concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) }) } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt new file mode 100644 index 0000000000..e88230de3d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/CustomSerializer.kt @@ -0,0 +1,105 @@ +package net.corda.core.serialization.amqp + +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type + +/** + * Base class for serializers of core platform types that do not conform to the usual serialization rules and thus + * cannot be automatically serialized. + */ +abstract class CustomSerializer : AMQPSerializer { + /** + * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects + * that refer to arrays of types etc. + */ + abstract val additionalSerializers: Iterable> + + abstract fun isSerializerFor(clazz: Class<*>): Boolean + protected abstract val descriptor: Descriptor + /** + * This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization + * code path. + */ + abstract val schemaForDocumentation: Schema + + override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { + data.withDescribed(descriptor) { + @Suppress("UNCHECKED_CAST") + writeDescribedObject(obj as T, data, type, output) + } + } + + abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) + + /** + * Additional base features for a custom serializer that is a particular class. + */ + abstract class Is(protected val clazz: Class) : CustomSerializer() { + override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz + override val type: Type get() = clazz + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" + override fun writeClassInfo(output: SerializationOutput) {} + override val descriptor: Descriptor = Descriptor(typeDescriptor) + } + + /** + * Additional base features for a custom serializer for all implementations of a particular interface or super class. + */ + abstract class Implements(protected val clazz: Class) : CustomSerializer() { + override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz) + override val type: Type get() = clazz + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" + override fun writeClassInfo(output: SerializationOutput) {} + override val descriptor: Descriptor = Descriptor(typeDescriptor) + } + + /** + * Addition base features over and above [Implements] or [Is] custom serializer for when the serialize form should be + * the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization. + * + * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered + * custom serializers. + */ + abstract class Proxy(protected val clazz: Class, + protected val proxyClass: Class

, + protected val factory: SerializerFactory, + val withInheritance: Boolean = true) : CustomSerializer() { + override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz + override val type: Type get() = clazz + override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" + override fun writeClassInfo(output: SerializationOutput) {} + override val descriptor: Descriptor = Descriptor(typeDescriptor) + + private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } + + override val schemaForDocumentation: Schema by lazy { + val typeNotations = mutableSetOf(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields)) + for (additional in additionalSerializers) { + typeNotations.addAll(additional.schemaForDocumentation.types) + } + Schema(typeNotations.toList()) + } + + /** + * Implement these two methods. + */ + protected abstract fun toProxy(obj: T): P + + protected abstract fun fromProxy(proxy: P): T + + override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) { + val proxy = toProxy(obj) + data.withList { + for (property in proxySerializer.propertySerializers) { + property.writeProperty(proxy, this, output) + } + } + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T { + @Suppress("UNCHECKED_CAST") + val proxy = proxySerializer.readObject(obj, schema, input) as P + return fromProxy(proxy) + } + } +} diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt index b47d75b8bc..ccbe1fac20 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializationInput.kt @@ -15,7 +15,7 @@ import java.util.* * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class DeserializationInput(private val serializerFactory: SerializerFactory = SerializerFactory()) { +class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { // TODO: we're not supporting object refs yet private val objectHistory: MutableList = ArrayList() @@ -41,7 +41,7 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se } val envelope = Envelope.get(data) // Now pick out the obj and schema from the envelope. - return clazz.cast(readObjectOrNull(envelope.obj, envelope, clazz)) + return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) } catch(nse: NotSerializableException) { throw nse } catch(t: Throwable) { @@ -51,20 +51,21 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se } } - internal fun readObjectOrNull(obj: Any?, envelope: Envelope, type: Type): Any? { + internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? { if (obj == null) { return null } else { - return readObject(obj, envelope, type) + return readObject(obj, schema, type) } } - internal fun readObject(obj: Any, envelope: Envelope, type: Type): Any { + internal fun readObject(obj: Any, schema: Schema, type: Type): Any { if (obj is DescribedType) { // Look up serializer in factory by descriptor - val serializer = serializerFactory.get(obj.descriptor, envelope) - if (serializer.type != type && !serializer.type.isSubClassOf(type)) throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type") - return serializer.readObject(obj.described, envelope, this) + val serializer = serializerFactory.get(obj.descriptor, schema) + if (serializer.type != type && !serializer.type.isSubClassOf(type)) + throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type") + return serializer.readObject(obj.described, schema, this) } else { return obj } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt index 2cd0ae1298..9a0809d18d 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/DeserializedParameterizedType.kt @@ -119,7 +119,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p private fun makeType(typeName: String, cl: ClassLoader): Type { // Not generic - return if (typeName == "*") SerializerFactory.AnyType else Class.forName(typeName, false, cl) + return if (typeName == "?") SerializerFactory.AnyType else Class.forName(typeName, false, cl) } private fun makeParameterizedType(rawTypeName: String, args: MutableList, cl: ClassLoader): Type { diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt index 2ea61c6598..7991648f1a 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/MapSerializer.kt @@ -13,10 +13,9 @@ import kotlin.collections.map /** * Serialization / deserialization of certain supported [Map] types. */ -class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer { +class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer { override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString()) - private val typeName = declaredType.toString() - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}" + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" companion object { private val supportedTypes: Map>, (Map<*, *>) -> Map<*, *>> = mapOf( @@ -24,15 +23,15 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer { SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) }, NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) } ) + + private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { + return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") + } } private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>) - private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> { - return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.") - } - - private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) + private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) override fun writeClassInfo(output: SerializationOutput) { if (output.writeTypeNotations(typeNotation)) { @@ -56,11 +55,13 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer { } } - override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { // TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole? - val entries: Iterable> = (obj as Map<*, *>).map { readEntry(envelope, input, it) } + val entries: Iterable> = (obj as Map<*, *>).map { readEntry(schema, input, it) } return concreteBuilder(entries.toMap()) } - private fun readEntry(envelope: Envelope, input: DeserializationInput, entry: Map.Entry) = input.readObjectOrNull(entry.key, envelope, declaredType.actualTypeArguments[0]) to input.readObjectOrNull(entry.value, envelope, declaredType.actualTypeArguments[1]) + private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry) = + input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to + input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1]) } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt index 2ccfad81d6..130d50d7a3 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/ObjectSerializer.kt @@ -10,26 +10,30 @@ import kotlin.reflect.jvm.javaConstructor /** * Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor). */ -class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer { +class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSerializer { override val type: Type get() = clazz private val javaConstructor: Constructor? - private val propertySerializers: Collection + internal val propertySerializers: Collection init { val kotlinConstructor = constructorForDeserialization(clazz) javaConstructor = kotlinConstructor?.javaConstructor - propertySerializers = propertiesForSerialization(kotlinConstructor, clazz) + propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory) } private val typeName = clazz.name - override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}" + override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict. - private val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) + internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields()) override fun writeClassInfo(output: SerializationOutput) { - output.writeTypeNotations(typeNotation) - for (iface in interfaces) { - output.requireSerializer(iface) + if (output.writeTypeNotations(typeNotation)) { + for (iface in interfaces) { + output.requireSerializer(iface) + } + for (property in propertySerializers) { + property.writeClassInfo(output) + } } } @@ -45,13 +49,13 @@ class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer { } } - override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any { + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any { if (obj is UnsignedInteger) { // TODO: Object refs TODO("not implemented") //To change body of created functions use File | Settings | File Templates. } else if (obj is List<*>) { if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName") - val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, envelope, input) } + val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) } return construct(params) } else throw NotSerializableException("Body of described type is unexpected $obj") } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt index 50cb6c5581..2295a07b45 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/PropertySerializer.kt @@ -9,8 +9,9 @@ import kotlin.reflect.jvm.javaGetter * Base class for serialization of a property of an object. */ sealed class PropertySerializer(val name: String, val readMethod: Method) { + abstract fun writeClassInfo(output: SerializationOutput) abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) - abstract fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? + abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? val type: String = generateType() val requires: List = generateRequires() @@ -53,13 +54,13 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { } companion object { - fun make(name: String, readMethod: Method): PropertySerializer { + fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer { val type = readMethod.genericReturnType if (SerializerFactory.isPrimitive(type)) { // This is a little inefficient for performance since it does a runtime check of type. We could do build time check with lots of subclasses here. return AMQPPrimitivePropertySerializer(name, readMethod) } else { - return DescribedTypePropertySerializer(name, readMethod) + return DescribedTypePropertySerializer(name, readMethod) { factory.get(null, type) } } } } @@ -67,9 +68,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { /** * A property serializer for a complex type (another object). */ - class DescribedTypePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) { - override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? { - return input.readObjectOrNull(obj, envelope, readMethod.genericReturnType) + class DescribedTypePropertySerializer(name: String, readMethod: Method, private val lazyTypeSerializer: () -> AMQPSerializer) : PropertySerializer(name, readMethod) { + // This is lazy so we don't get an infinite loop when a method returns an instance of the class. + private val typeSerializer: AMQPSerializer by lazy { lazyTypeSerializer() } + + override fun writeClassInfo(output: SerializationOutput) { + typeSerializer.writeClassInfo(output) + } + + override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { + return input.readObjectOrNull(obj, schema, readMethod.genericReturnType) } override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { @@ -81,7 +89,9 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) { * A property serializer for an AMQP primitive type (Int, String, etc). */ class AMQPPrimitivePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) { - override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? { + override fun writeClassInfo(output: SerializationOutput) {} + + override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { return obj } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt index 64a28a7aae..5c627cc943 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/Schema.kt @@ -87,7 +87,7 @@ data class Schema(val types: List) : DescribedType { override fun toString(): String = types.joinToString("\n") } -data class Descriptor(val name: String?, val code: UnsignedLong?) : DescribedType { +data class Descriptor(val name: String?, val code: UnsignedLong? = null) : DescribedType { companion object : DescribedTypeConstructor { val DESCRIPTOR = UnsignedLong(3L or DESCRIPTOR_TOP_32BITS) @@ -320,9 +320,9 @@ private val ANY_TYPE_HASH: String = "Any type = true" * different. */ // TODO: write tests -internal fun fingerprintForType(type: Type): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher()).hash().asBytes()) +internal fun fingerprintForType(type: Type, factory: SerializerFactory): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes()) -private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher: Hasher): Hasher { +private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher: Hasher, factory: SerializerFactory): Hasher { return if (type in alreadySeen) { hasher.putUnencodedChars(ALREADY_SEEN_HASH) } else { @@ -331,25 +331,31 @@ private fun fingerprintForType(type: Type, alreadySeen: MutableSet, hasher hasher.putUnencodedChars(ANY_TYPE_HASH) } else if (type is Class<*>) { if (type.isArray) { - fingerprintForType(type.componentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH) + fingerprintForType(type.componentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH) } else if (SerializerFactory.isPrimitive(type)) { hasher.putUnencodedChars(type.name) } else if (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) { hasher.putUnencodedChars(type.name) } else { - // Hash the class + properties + interfaces - propertiesForSerialization(constructorForDeserialization(type), type).fold(hasher.putUnencodedChars(type.name)) { orig, param -> - fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) + // Need to check if a custom serializer is applicable + val customSerializer = factory.findCustomSerializer(type) + if (customSerializer == null) { + // Hash the class + properties + interfaces + propertiesForSerialization(constructorForDeserialization(type), type, factory).fold(hasher.putUnencodedChars(type.name)) { orig, param -> + fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig, factory).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH) + } + interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher, factory) } + hasher + } else { + hasher.putUnencodedChars(customSerializer.typeDescriptor) } - interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher) } - hasher } } else if (type is ParameterizedType) { // Hash the rawType + params - type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig) } + type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher, factory)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig, factory) } } else if (type is GenericArrayType) { // Hash the element type + some array hash - fingerprintForType(type.genericComponentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH) + fingerprintForType(type.genericComponentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH) } else { throw NotSerializableException("Don't know how to hash $type") } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt index 107769cde7..85082544a4 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationHelper.kt @@ -1,14 +1,16 @@ package net.corda.core.serialization.amqp +import com.google.common.reflect.TypeToken import org.apache.qpid.proton.codec.Data import java.beans.Introspector -import java.beans.PropertyDescriptor import java.io.NotSerializableException +import java.lang.reflect.Method import java.lang.reflect.Modifier import java.lang.reflect.ParameterizedType import java.lang.reflect.Type import kotlin.reflect.KClass import kotlin.reflect.KFunction +import kotlin.reflect.KParameter import kotlin.reflect.full.findAnnotation import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.javaType @@ -58,24 +60,26 @@ internal fun constructorForDeserialization(clazz: Class): KFunction * Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have * names accessible via reflection. */ -internal fun propertiesForSerialization(kotlinConstructor: KFunction?, clazz: Class<*>): Collection { - return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor) else propertiesForSerialization(clazz) +internal fun propertiesForSerialization(kotlinConstructor: KFunction?, clazz: Class<*>, factory: SerializerFactory): Collection { + return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory) } private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) -private fun propertiesForSerialization(kotlinConstructor: KFunction): Collection { +private fun propertiesForSerialization(kotlinConstructor: KFunction, factory: SerializerFactory): Collection { val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. - val properties: Map = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] } + val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] } val rc: MutableList = ArrayList(kotlinConstructor.parameters.size) for (param in kotlinConstructor.parameters) { val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.") - val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz. If using Java, check that you have the -parameters option specified in the Java compiler.") + val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz." + + " If using Java, check that you have the -parameters option specified in the Java compiler.") // Check that the method has a getter in java. - val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") - if (getter.genericReturnType == param.type.javaType) { - rc += PropertySerializer.make(name, getter) + val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + + " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") + if (constructorParamTakesReturnTypeOfGetter(getter, param)) { + rc += PropertySerializer.make(name, getter, factory) } else { throw NotSerializableException("Property type ${getter.genericReturnType} for $name of $clazz differs from constructor parameter type ${param.type.javaType}") } @@ -83,14 +87,16 @@ private fun propertiesForSerialization(kotlinConstructor: KFunction return rc } -private fun propertiesForSerialization(clazz: Class<*>): Collection { +private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType) + +private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection { // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name } val rc: MutableList = ArrayList(properties.size) for (property in properties) { // Check that the method has a getter in java. val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.") - rc += PropertySerializer.make(property.name, getter) + rc += PropertySerializer.make(property.name, getter, factory) } return rc } @@ -104,6 +110,7 @@ internal fun interfacesForSerialization(clazz: Class<*>): List { private fun exploreType(type: Type?, interfaces: MutableSet) { val clazz = (type as? Class<*>) ?: (type as? ParameterizedType)?.rawType as? Class<*> if (clazz != null) { + if (clazz.isInterface) interfaces += clazz for (newInterface in clazz.genericInterfaces) { if (newInterface !in interfaces) { interfaces += newInterface diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt index f440d62c2a..3cbfad41ba 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializationOutput.kt @@ -14,10 +14,10 @@ import kotlin.collections.LinkedHashSet * @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple * instances and threads. */ -class SerializationOutput(private val serializerFactory: SerializerFactory = SerializerFactory()) { +open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactory()) { // TODO: we're not supporting object refs yet private val objectHistory: MutableMap = IdentityHashMap() - private val serializerHistory: MutableSet = LinkedHashSet() + private val serializerHistory: MutableSet> = LinkedHashSet() private val schemaHistory: MutableSet = LinkedHashSet() /** @@ -64,19 +64,21 @@ class SerializationOutput(private val serializerFactory: SerializerFactory = Ser internal fun writeObject(obj: Any, data: Data, type: Type) { val serializer = serializerFactory.get(obj.javaClass, type) if (serializer !in serializerHistory) { + serializerHistory.add(serializer) serializer.writeClassInfo(this) } serializer.writeObject(obj, data, type, this) } - internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean { + open internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean { return schemaHistory.addAll(typeNotation) } - internal fun requireSerializer(type: Type) { - if (type != SerializerFactory.AnyType) { + open internal fun requireSerializer(type: Type) { + if (type != SerializerFactory.AnyType && type != Object::class.java) { val serializer = serializerFactory.get(null, type) if (serializer !in serializerHistory) { + serializerHistory.add(serializer) serializer.writeClassInfo(this) } } diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt index 1456c9a7ca..3883aad9dd 100644 --- a/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/SerializerFactory.kt @@ -10,18 +10,19 @@ import java.io.NotSerializableException import java.lang.reflect.GenericArrayType import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import java.lang.reflect.WildcardType import java.util.* import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList import javax.annotation.concurrent.ThreadSafe /** * Factory of serializers designed to be shared across threads and invocations. */ +// TODO: enums // TODO: object references // TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal) // TODO: Inner classes etc -// TODO: support for custom serialisation of core types (of e.g. PublicKey, Throwables) -// TODO: exclude schemas for core types that don't need custom serializers that everyone already knows the schema for. // TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency // TODO: maybe support for caching of serialized form of some core types for performance // TODO: profile for performance in general @@ -30,10 +31,13 @@ import javax.annotation.concurrent.ThreadSafe // TODO: incorporate the class carpenter for classes not on the classpath. // TODO: apply class loader logic and an "app context" throughout this code. // TODO: schema evolution solution when the fingerprints do not line up. +// TODO: allow definition of well known types that are left out of the schema. +// TODO: automatically support byte[] without having to wrap in [Binary]. @ThreadSafe class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { - private val serializersByType = ConcurrentHashMap() - private val serializersByDescriptor = ConcurrentHashMap() + private val serializersByType = ConcurrentHashMap>() + private val serializersByDescriptor = ConcurrentHashMap>() + private val customSerializers = CopyOnWriteArrayList>() /** * Look up, and manufacture if necessary, a serializer for the given type. @@ -42,7 +46,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { * restricted type processing). */ @Throws(NotSerializableException::class) - fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer { + fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer { if (declaredType is ParameterizedType) { return serializersByType.computeIfAbsent(declaredType) { // We allow only Collection and Map. @@ -50,7 +54,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { if (rawType is Class<*>) { checkParameterisedTypesConcrete(declaredType.actualTypeArguments) if (Collection::class.java.isAssignableFrom(rawType)) { - CollectionSerializer(declaredType) + CollectionSerializer(declaredType, this) } else if (Map::class.java.isAssignableFrom(rawType)) { makeMapSerializer(declaredType) } else { @@ -63,27 +67,44 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } else if (declaredType is Class<*>) { // Simple classes allowed if (Collection::class.java.isAssignableFrom(declaredType)) { - return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null)) } + return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null), this) } } else if (Map::class.java.isAssignableFrom(declaredType)) { return serializersByType.computeIfAbsent(declaredType) { makeMapSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType, AnyType), null)) } } else { return makeClassSerializer(actualType ?: declaredType) } } else if (declaredType is GenericArrayType) { - return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType) } + return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) } } else { throw NotSerializableException("Declared types of $declaredType are not supported.") } } + /** + * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types + * contained in the [Schema]. + */ @Throws(NotSerializableException::class) - fun get(typeDescriptor: Any, envelope: Envelope): AMQPSerializer { + fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer { return serializersByDescriptor[typeDescriptor] ?: { - processSchema(envelope.schema) + processSchema(schema) serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.") }() } + /** + * TODO: Add docs + */ + fun register(customSerializer: CustomSerializer) { + if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) { + customSerializers += customSerializer + serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer + for (additional in customSerializer.additionalSerializers) { + register(additional) + } + } + } + private fun processSchema(schema: Schema) { for (typeNotation in schema.types) { processSchemaEntry(typeNotation) @@ -99,7 +120,14 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { private fun restrictedTypeForName(name: String): Type { return if (name.endsWith("[]")) { - DeserializedGenericArrayType(restrictedTypeForName(name.substring(0, name.lastIndex - 1))) + val elementType = restrictedTypeForName(name.substring(0, name.lastIndex - 1)) + if (elementType is ParameterizedType || elementType is GenericArrayType) { + DeserializedGenericArrayType(elementType) + } else if (elementType is Class<*>) { + java.lang.reflect.Array.newInstance(elementType, 0).javaClass + } else { + throw NotSerializableException("Not able to deserialize array type: $name") + } } else { DeserializedParameterizedType.make(name) } @@ -134,32 +162,52 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { } } - private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer { + private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer { return serializersByType.computeIfAbsent(clazz) { - if (clazz.isArray) { - whitelisted(clazz.componentType) - ArraySerializer(clazz) - } else if (isPrimitive(clazz)) { + if (isPrimitive(clazz)) { AMQPPrimitiveSerializer(clazz) } else { - whitelisted(clazz) - ObjectSerializer(clazz) + findCustomSerializer(clazz) ?: { + if (clazz.isArray) { + whitelisted(clazz.componentType) + ArraySerializer(clazz, this) + } else { + whitelisted(clazz) + ObjectSerializer(clazz, this) + } + }() } } } + internal fun findCustomSerializer(clazz: Class<*>): AMQPSerializer? { + for (customSerializer in customSerializers) { + if (customSerializer.isSerializerFor(clazz)) { + return customSerializer + } + } + return null + } + private fun whitelisted(clazz: Class<*>): Boolean { - if (whitelist.hasListed(clazz) || clazz.isAnnotationPresent(CordaSerializable::class.java)) { + if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) { return true } else { throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.") } } - private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { + // Recursively check the class, interfaces and superclasses for our annotation. + internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean { + return type.isAnnotationPresent(CordaSerializable::class.java) || + type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationInHierarchy(it) } + || (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) + } + + private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer { val rawType = declaredType.rawType as Class<*> rawType.checkNotUnorderedHashMap() - return MapSerializer(declaredType) + return MapSerializer(declaredType, this) } companion object { @@ -185,12 +233,17 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { Char::class.java to "char", Date::class.java to "timestamp", UUID::class.java to "uuid", - ByteArray::class.java to "binary", + Binary::class.java to "binary", String::class.java to "string", Symbol::class.java to "symbol") } - object AnyType : Type { - override fun toString(): String = "*" + object AnyType : WildcardType { + override fun getUpperBounds(): Array = arrayOf(Object::class.java) + + override fun getLowerBounds(): Array = emptyArray() + + override fun toString(): String = "?" } } + diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt new file mode 100644 index 0000000000..46536a1bed --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/PublicKeySerializer.kt @@ -0,0 +1,24 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.crypto.Crypto +import net.corda.core.serialization.amqp.* +import org.apache.qpid.proton.amqp.Binary +import org.apache.qpid.proton.codec.Data +import java.lang.reflect.Type +import java.security.PublicKey + +class PublicKeySerializer : CustomSerializer.Implements(PublicKey::class.java) { + override val additionalSerializers: Iterable> = emptyList() + + override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(Binary::class.java)!!, descriptor, emptyList()))) + + override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput) { + // TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser. + output.writeObject(Binary(obj.encoded), data, clazz) + } + + override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey { + val A = input.readObject(obj, schema, ByteArray::class.java) as Binary + return Crypto.decodePublicKey(A.array) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt new file mode 100644 index 0000000000..ed267ed44d --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/serialization/amqp/custom/ThrowableSerializer.kt @@ -0,0 +1,81 @@ +package net.corda.core.serialization.amqp.custom + +import net.corda.core.serialization.amqp.CustomSerializer +import net.corda.core.serialization.amqp.SerializerFactory +import net.corda.core.serialization.amqp.constructorForDeserialization +import net.corda.core.serialization.amqp.propertiesForSerialization +import net.corda.core.utilities.CordaRuntimeException +import net.corda.core.utilities.CordaThrowable +import java.io.NotSerializableException + +class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(Throwable::class.java, ThrowableProxy::class.java, factory) { + override val additionalSerializers: Iterable> = listOf(StackTraceElementSerializer(factory)) + + override fun toProxy(obj: Throwable): ThrowableProxy { + val extraProperties: MutableMap = LinkedHashMap() + val message = if (obj is CordaThrowable) { + // Try and find a constructor + try { + val constructor = constructorForDeserialization(obj.javaClass) + val props = propertiesForSerialization(constructor, obj.javaClass, factory) + for (prop in props) { + extraProperties[prop.name] = prop.readMethod.invoke(obj) + } + } catch(e: NotSerializableException) { + } + obj.originalMessage + } else { + obj.message + } + return ThrowableProxy(obj.javaClass.name, message, obj.stackTrace, obj.cause, obj.suppressed, extraProperties) + } + + override fun fromProxy(proxy: ThrowableProxy): Throwable { + try { + // TODO: This will need reworking when we have multiple class loaders + val clazz = Class.forName(proxy.exceptionClass, false, this.javaClass.classLoader) + // If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties + // Otherwise we just make a CordaRuntimeException + if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) { + val constructor = constructorForDeserialization(clazz)!! + val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap()) + (throwable as CordaThrowable).apply { + if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass + this.setMessage(proxy.message) + this.setCause(proxy.cause) + this.addSuppressed(proxy.suppressed) + } + return (throwable as Throwable).apply { + this.stackTrace = proxy.stackTrace + } + } + } catch (e: Exception) { + // If attempts to rebuild the exact exception fail, we fall through and build a runtime exception. + } + // If the criteria are not met or we experience an exception constructing the exception, we fall back to our own unchecked exception. + return CordaRuntimeException(proxy.exceptionClass).apply { + this.setMessage(proxy.message) + this.setCause(proxy.cause) + this.stackTrace = proxy.stackTrace + this.addSuppressed(proxy.suppressed) + } + } + + class ThrowableProxy( + val exceptionClass: String, + val message: String?, + val stackTrace: Array, + val cause: Throwable?, + val suppressed: Array, + val additionalProperties: Map) +} + +class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) { + override val additionalSerializers: Iterable> = emptyList() + + override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber) + + override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber) + + data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int) +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt index 61010282e8..876efdda82 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/BaseTransaction.kt @@ -36,12 +36,12 @@ abstract class BaseTransaction( * If specified, a time window in which this transaction may have been notarised. Contracts can check this * time window to find out when a transaction is deemed to have occurred, from the ledger's perspective. */ - val timestamp: Timestamp? + val timeWindow: TimeWindow? ) : NamedByHash { protected fun checkInvariants() { - if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." } - if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." } + if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" } + if (timeWindow != null) check(notary != null) { "If a time-window is provided, there must be a notary" } } override fun equals(other: Any?): Boolean { @@ -50,10 +50,10 @@ abstract class BaseTransaction( notary == other.notary && mustSign == other.mustSign && type == other.type && - timestamp == other.timestamp + timeWindow == other.timeWindow } - override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp) + override fun hashCode() = Objects.hash(notary, mustSign, type, timeWindow) override fun toString(): String = "${javaClass.simpleName}(id=$id)" } diff --git a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt index 4665387c7d..3fd91f1f0e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/LedgerTransaction.kt @@ -32,9 +32,9 @@ class LedgerTransaction( override val id: SecureHash, notary: Party?, signers: List, - timestamp: Timestamp?, + timeWindow: TimeWindow?, type: TransactionType -) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) { +) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow) { init { checkInvariants() } @@ -47,7 +47,7 @@ class LedgerTransaction( /** Strips the transaction down to a form that is usable by the contract verify functions */ fun toTransactionForContract(): TransactionForContract { return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id, - inputs.map { it.state.notary }.singleOrNull(), timestamp) + inputs.map { it.state.notary }.singleOrNull(), timeWindow) } /** 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 78c25f7154..0e611fb242 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/MerkleTransaction.kt @@ -11,6 +11,7 @@ import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.serialize import net.corda.core.serialization.withoutReferences import java.security.PublicKey +import java.util.function.Predicate fun serializedHash(x: T): SecureHash { return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } } @@ -33,7 +34,7 @@ interface TraversableTransaction { val notary: Party? val mustSign: List val type: TransactionType? - val timestamp: Timestamp? + val timeWindow: TimeWindow? /** * Returns a flattened list of all the components that are present in the transaction, in the following order: @@ -45,7 +46,7 @@ interface TraversableTransaction { * - The notary [Party], if present * - Each required signer ([mustSign]) that is present * - The type of the transaction, if present - * - The timestamp of the transaction, if present + * - The time-window of the transaction, if present */ val availableComponents: List get() { @@ -56,7 +57,7 @@ interface TraversableTransaction { notary?.let { result += it } result.addAll(mustSign) type?.let { result += it } - timestamp?.let { result += it } + timeWindow?.let { result += it } return result } @@ -81,7 +82,7 @@ class FilteredLeaves( override val notary: Party?, override val mustSign: List, override val type: TransactionType?, - override val timestamp: Timestamp? + override val timeWindow: TimeWindow? ) : TraversableTransaction { /** * Function that checks the whole filtered structure. @@ -116,8 +117,9 @@ class FilteredTransaction private constructor( * @param wtx WireTransaction to be filtered. * @param filtering filtering over the whole WireTransaction */ + @JvmStatic fun buildMerkleTransaction(wtx: WireTransaction, - filtering: (Any) -> Boolean + filtering: Predicate ): FilteredTransaction { val filteredLeaves = wtx.filterWithFun(filtering) val merkleTree = wtx.merkleTree 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 e100c89793..00f83fdf3e 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt @@ -26,9 +26,11 @@ import java.util.* * when verifying composite key signatures, but may be used as individual signatures where a single key is expected to * sign. */ +// DOCSTART 1 data class SignedTransaction(val txBits: SerializedBytes, val sigs: List ) : NamedByHash { +// DOCEND 1 init { require(sigs.isNotEmpty()) } @@ -64,8 +66,10 @@ data class SignedTransaction(val txBits: SerializedBytes, * @throws SignatureException if any signatures are invalid or unrecognised. * @throws SignaturesMissingException if any signatures should have been present but were not. */ + // DOCSTART 2 @Throws(SignatureException::class) fun verifySignatures(vararg allowedToBeMissing: PublicKey): WireTransaction { + // DOCEND 2 // Embedded WireTransaction is not deserialised until after we check the signatures. checkSignaturesAreValid() diff --git a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt index 19396b1e01..e727ad7c7c 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt @@ -3,7 +3,7 @@ package net.corda.core.transactions import co.paralleluniverse.strands.Strand import net.corda.core.contracts.* import net.corda.core.crypto.* -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.FlowStateMachine import net.corda.core.identity.Party import net.corda.core.serialization.serialize import java.security.KeyPair @@ -37,9 +37,10 @@ open class TransactionBuilder( protected val outputs: MutableList> = arrayListOf(), protected val commands: MutableList = arrayListOf(), protected val signers: MutableSet = mutableSetOf(), - protected var timestamp: Timestamp? = null) { + protected var timeWindow: TimeWindow? = null) { + constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) - val time: Timestamp? get() = timestamp + val time: TimeWindow? get() = timeWindow // TODO: rename using a more descriptive name (i.e. timeWindowGetter) or remove if unused. /** * Creates a copy of the builder. @@ -53,30 +54,31 @@ open class TransactionBuilder( outputs = ArrayList(outputs), commands = ArrayList(commands), signers = LinkedHashSet(signers), - timestamp = timestamp + timeWindow = timeWindow ) /** - * Places a [TimestampCommand] in this transaction, removing any existing command if there is one. + * Places a [TimeWindow] in this transaction, removing any existing command if there is one. * The command requires a signature from the Notary service, which acts as a Timestamp Authority. * The signature can be obtained using [NotaryFlow]. * - * The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance]. + * The window of time in which the final time-window may lie is defined as [time] +/- [timeTolerance]. * If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance * should be chosen such that your code can finish building the transaction and sending it to the TSA within that * window of time, taking into account factors such as network latency. Transactions being built by a group of * collaborating parties may therefore require a higher time tolerance than a transaction being built by a single * node. */ - fun setTime(time: Instant, timeTolerance: Duration) = setTime(Timestamp(time, timeTolerance)) + fun addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance)) - fun setTime(newTimestamp: Timestamp) { - check(notary != null) { "Only notarised transactions can have a timestamp" } + fun addTimeWindow(timeWindow: TimeWindow) { + check(notary != null) { "Only notarised transactions can have a time-window" } signers.add(notary!!.owningKey) - check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" } - this.timestamp = newTimestamp + check(currentSigs.isEmpty()) { "Cannot change time-window after signing" } + this.timeWindow = timeWindow } + // DOCSTART 1 /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ fun withItems(vararg items: Any): TransactionBuilder { for (t in items) { @@ -91,10 +93,12 @@ open class TransactionBuilder( } return this } + // DOCEND 1 /** The signatures that have been collected so far - might be incomplete! */ protected val currentSigs = arrayListOf() + @Deprecated("Use ServiceHub.signInitialTransaction() instead.") fun signWith(key: KeyPair): TransactionBuilder { check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" } val data = toWireTransaction().id @@ -132,7 +136,7 @@ open class TransactionBuilder( } fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), - ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timestamp) + ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow) fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { if (checkSufficientSignatures) { diff --git a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt index 09e2b9218b..ecc2c58be9 100644 --- a/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt +++ b/core/src/main/kotlin/net/corda/core/transactions/WireTransaction.kt @@ -13,6 +13,7 @@ import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.serialize import net.corda.core.utilities.Emoji import java.security.PublicKey +import java.util.function.Predicate /** * A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped @@ -31,8 +32,8 @@ class WireTransaction( notary: Party?, signers: List, type: TransactionType, - timestamp: Timestamp? -) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp), TraversableTransaction { + timeWindow: TimeWindow? +) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow), TraversableTransaction { init { checkInvariants() } @@ -100,13 +101,13 @@ class WireTransaction( val resolvedInputs = inputs.map { ref -> resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash) } - return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type) + return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timeWindow, type) } /** * Build filtered transaction using provided filtering functions. */ - fun buildFilteredTransaction(filtering: (Any) -> Boolean): FilteredTransaction { + fun buildFilteredTransaction(filtering: Predicate): FilteredTransaction { return FilteredTransaction.buildMerkleTransaction(this, filtering) } @@ -120,17 +121,17 @@ class WireTransaction( * @param filtering filtering over the whole WireTransaction * @returns FilteredLeaves used in PartialMerkleTree calculation and verification. */ - fun filterWithFun(filtering: (Any) -> Boolean): FilteredLeaves { - fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering(elem)) null else elem + fun filterWithFun(filtering: Predicate): FilteredLeaves { + fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering.test(elem)) null else elem return FilteredLeaves( - inputs.filter { filtering(it) }, - attachments.filter { filtering(it) }, - outputs.filter { filtering(it) }, - commands.filter { filtering(it) }, + inputs.filter { filtering.test(it) }, + attachments.filter { filtering.test(it) }, + outputs.filter { filtering.test(it) }, + commands.filter { filtering.test(it) }, notNullFalse(notary) as Party?, - mustSign.filter { filtering(it) }, + mustSign.filter { filtering.test(it) }, notNullFalse(type) as TransactionType?, - notNullFalse(timestamp) as Timestamp? + notNullFalse(timeWindow) as TimeWindow? ) } diff --git a/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt b/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt new file mode 100644 index 0000000000..907bbee408 --- /dev/null +++ b/core/src/main/kotlin/net/corda/core/utilities/CordaException.kt @@ -0,0 +1,103 @@ +package net.corda.core.utilities + +import net.corda.core.serialization.CordaSerializable +import java.util.* + +@CordaSerializable +interface CordaThrowable { + var originalExceptionClassName: String? + val originalMessage: String? + fun setMessage(message: String?) + fun setCause(cause: Throwable?) + fun addSuppressed(suppressed: Array) +} + +open class CordaException internal constructor(override var originalExceptionClassName: String? = null, + private var _message: String? = null, + private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable { + + constructor(message: String?, + cause: Throwable?) : this(null, message, cause) + + override val message: String? + get() = if (originalExceptionClassName == null) originalMessage else { + if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" + } + + override val cause: Throwable? + get() = _cause ?: super.cause + + override fun setMessage(message: String?) { + _message = message + } + + override fun setCause(cause: Throwable?) { + _cause = cause + } + + override fun addSuppressed(suppressed: Array) { + for (suppress in suppressed) { + addSuppressed(suppress) + } + } + + override val originalMessage: String? + get() = _message + + override fun hashCode(): Int { + return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage) + } + + override fun equals(other: Any?): Boolean { + return other is CordaException && + originalExceptionClassName == other.originalExceptionClassName && + message == other.message && + cause == other.cause && + Arrays.equals(stackTrace, other.stackTrace) && + Arrays.equals(suppressed, other.suppressed) + } +} + +open class CordaRuntimeException internal constructor(override var originalExceptionClassName: String?, + private var _message: String? = null, + private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable { + constructor(message: String?, cause: Throwable?) : this(null, message, cause) + + override val message: String? + get() = if (originalExceptionClassName == null) originalMessage else { + if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage" + } + + override val cause: Throwable? + get() = _cause ?: super.cause + + override fun setMessage(message: String?) { + _message = message + } + + override fun setCause(cause: Throwable?) { + _cause = cause + } + + override fun addSuppressed(suppressed: Array) { + for (suppress in suppressed) { + addSuppressed(suppress) + } + } + + override val originalMessage: String? + get() = _message + + override fun hashCode(): Int { + return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage) + } + + override fun equals(other: Any?): Boolean { + return other is CordaRuntimeException && + originalExceptionClassName == other.originalExceptionClassName && + message == other.message && + cause == other.cause && + Arrays.equals(stackTrace, other.stackTrace) && + Arrays.equals(suppressed, other.suppressed) + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt index c51aa1f46e..bc7fd86a14 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/Emoji.kt @@ -7,8 +7,12 @@ import net.corda.core.codePointsString */ object Emoji { // Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default. + // However the JediTerm java terminal emulator can also do emoji on OS X when using the JetBrains JRE. + // Check for that here. DemoBench sets TERM_PROGRAM appropriately. val hasEmojiTerminal by lazy { - System.getenv("CORDA_FORCE_EMOJI") != null || System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") + System.getenv("CORDA_FORCE_EMOJI") != null || + System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") || + (System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o") } @JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385) diff --git a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt index 308824def6..d69ab38367 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/ProcessUtilities.kt @@ -2,21 +2,24 @@ package net.corda.core.utilities import java.nio.file.Path +// TODO This doesn't belong in core and can be moved into node object ProcessUtilities { inline fun startJavaProcess( arguments: List, + classpath: String = defaultClassPath, jdwpPort: Int? = null, extraJvmArguments: List = emptyList(), inheritIO: Boolean = true, errorLogPath: Path? = null, workingDirectory: Path? = null ): Process { - return startJavaProcess(C::class.java.name, arguments, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory) + return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory) } fun startJavaProcess( className: String, arguments: List, + classpath: String = defaultClassPath, jdwpPort: Int? = null, extraJvmArguments: List = emptyList(), inheritIO: Boolean = true, @@ -24,7 +27,6 @@ object ProcessUtilities { workingDirectory: Path? = null ): Process { val separator = System.getProperty("file.separator") - val classpath = System.getProperty("java.class.path") val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java" val debugPortArgument = if (jdwpPort != null) { listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort") @@ -44,4 +46,6 @@ object ProcessUtilities { if (workingDirectory != null) directory(workingDirectory.toFile()) }.start() } + + val defaultClassPath: String get() = System.getProperty("java.class.path") } diff --git a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt b/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt index 1084808a4a..6861fa7fc0 100644 --- a/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt +++ b/core/src/main/kotlin/net/corda/core/utilities/TestConstants.kt @@ -4,10 +4,12 @@ package net.corda.core.utilities import net.corda.core.crypto.* import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import org.bouncycastle.asn1.x500.X500Name import java.math.BigInteger import java.security.KeyPair import java.security.PublicKey +import java.security.cert.CertificateFactory import java.time.Instant // A dummy time at which we will be pretending test transactions are created. @@ -21,6 +23,7 @@ val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() } val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) } /** Dummy notary identity for tests and simulations */ +val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY) val DUMMY_NOTARY: Party get() = Party(X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), DUMMY_NOTARY_KEY.public) val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) } @@ -29,7 +32,7 @@ val DUMMY_MAP: Party get() = Party(X500Name("CN=Network Map Service,O=R3,OU=cord val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) } /** Dummy bank identity for tests and simulations */ -val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=UK"), DUMMY_BANK_A_KEY.public) +val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=GB"), DUMMY_BANK_A_KEY.public) val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) } /** Dummy bank identity for tests and simulations */ @@ -41,16 +44,37 @@ val DUMMY_BANK_C: Party get() = Party(X500Name("CN=Bank C,O=Bank C,L=Tokyo,C=JP" val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) } /** Dummy individual identity for tests and simulations */ -val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=London,C=UK"), ALICE_KEY.public) +val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE) +val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), ALICE_KEY.public) val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) } /** Dummy individual identity for tests and simulations */ -val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=London,C=UK"), BOB_KEY.public) +val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB) +val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_KEY.public) val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) } /** Dummy individual identity for tests and simulations */ -val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=London,C=UK"), CHARLIE_KEY.public) +val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public) val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) } /** Dummy regulator for tests and simulations */ val DUMMY_REGULATOR: Party get() = Party(X500Name("CN=Regulator A,OU=Corda,O=AMF,L=Paris,C=FR"), DUMMY_REGULATOR_KEY.public) + +val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) } +val DUMMY_CA: CertificateAndKeyPair by lazy { + // TODO: Should be identity scheme + val cert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), DUMMY_CA_KEY) + CertificateAndKeyPair(cert, DUMMY_CA_KEY) +} + +/** + * Build a test party with a nonsense certificate authority for testing purposes. + */ +fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA) = getTestPartyAndCertificate(Party(name, publicKey), trustRoot) + +fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate { + val certFactory = CertificateFactory.getInstance("X509") + val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey) + val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert)) + return PartyAndCertificate(party, certHolder, certPath) +} diff --git a/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt b/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt deleted file mode 100644 index 2fb80ee61d..0000000000 --- a/core/src/main/kotlin/net/corda/core/utilities/TimeWindow.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.corda.core.utilities - -import java.time.Duration -import java.time.Instant - -/** - * A class representing a window in time from a particular instant, lasting a specified duration. - */ -data class TimeWindow(val start: Instant, val duration: Duration) { - val end: Instant - get() = start + duration -} diff --git a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt index acfbba8541..5bd80d9f98 100644 --- a/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt @@ -60,7 +60,6 @@ import java.security.PublicKey * @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() { @@ -123,6 +122,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, partyNode.legalIdentity } + // DOCSTART 1 /** * Get and check the required signature. */ @@ -132,6 +132,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction, it } } + // DOCEND 1 } /** diff --git a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt index ff62d89deb..e609c4f5ca 100644 --- a/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/FinalityFlow.kt @@ -89,7 +89,7 @@ class FinalityFlow(val transactions: Iterable, private fun needsNotarySignature(stx: SignedTransaction): Boolean { val wtx = stx.tx - val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timestamp != null + val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null return needsNotarisation && hasNoNotarySignature(stx) } diff --git a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt index 8de5b096e7..f99bcd2f3a 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryChangeFlow.kt @@ -7,7 +7,6 @@ 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.PublicKey /** * A flow to be used for changing a state's Notary. This is required since all input states to a transaction diff --git a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt index 32ff54484b..a12770ca1f 100644 --- a/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt +++ b/core/src/main/kotlin/net/corda/flows/NotaryFlow.kt @@ -2,7 +2,7 @@ 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.contracts.TimeWindow import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SignedData @@ -11,7 +11,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.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessException import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable @@ -19,17 +19,18 @@ import net.corda.core.serialization.serialize import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap +import java.util.function.Predicate object NotaryFlow { /** * A flow to be used by a party for obtaining signature(s) from a [NotaryService] ascertaining the transaction - * timestamp is correct and none of its inputs have been used in another completed transaction. + * time-window is correct and none of its inputs have been used in another completed transaction. * * In case of a single-node or Raft notary, the flow will return a single signature. For the BFT notary multiple * signatures will be returned – one from each replica that accepted the input state commit. * * @throws NotaryException in case the any of the inputs to the transaction have been consumed - * by another transaction or the timestamp is invalid. + * by another transaction or the time-window is invalid. */ @InitiatingFlow open class Client(private val stx: SignedTransaction, @@ -63,7 +64,7 @@ object NotaryFlow { val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) { stx } else { - wtx.buildFilteredTransaction { it is StateRef || it is Timestamp } + wtx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow }) } val response = try { @@ -90,19 +91,19 @@ object NotaryFlow { /** * A flow run by a notary service that handles notarisation requests. * - * It checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict + * It checks that the time-window command is valid (if present) and commits the input state, or returns a conflict * if any of the input states have been previously committed. * * Additional transaction validation logic can be added when implementing [receiveAndVerifyTx]. */ // See AbstractStateReplacementFlow.Acceptor for why it's Void? abstract class Service(val otherSide: Party, - val timestampChecker: TimestampChecker, + val timeWindowChecker: TimeWindowChecker, val uniquenessProvider: UniquenessProvider) : FlowLogic() { @Suspendable override fun call(): Void? { - val (id, inputs, timestamp) = receiveAndVerifyTx() - validateTimestamp(timestamp) + val (id, inputs, timeWindow) = receiveAndVerifyTx() + validateTimeWindow(timeWindow) commitInputStates(inputs, id) signAndSendResponse(id) return null @@ -121,9 +122,9 @@ object NotaryFlow { send(otherSide, listOf(signature)) } - private fun validateTimestamp(t: Timestamp?) { - if (t != null && !timestampChecker.isValid(t)) - throw NotaryException(NotaryError.TimestampInvalid) + private fun validateTimeWindow(t: TimeWindow?) { + if (t != null && !timeWindowChecker.isValid(t)) + throw NotaryException(NotaryError.TimeWindowInvalid) } /** @@ -162,7 +163,7 @@ object NotaryFlow { * The minimum amount of information needed to notarise a transaction. Note that this does not include * any sensitive transaction details. */ -data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: Timestamp?) +data class TransactionParts(val id: SecureHash, val inputs: List, val timestamp: TimeWindow?) class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error") @@ -172,8 +173,8 @@ sealed class NotaryError { override fun toString() = "One or more input states for transaction $txId have been used in another transaction" } - /** Thrown if the time specified in the timestamp command is outside the allowed tolerance */ - object TimestampInvalid : NotaryError() + /** Thrown if the time specified in the [TimeWindow] command is outside the allowed tolerance. */ + object TimeWindowInvalid : NotaryError() data class TransactionInvalid(val msg: String) : NotaryError() data class SignaturesInvalid(val msg: String) : NotaryError() diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt new file mode 100644 index 0000000000..ff50739ffc --- /dev/null +++ b/core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt @@ -0,0 +1,89 @@ +package net.corda.flows + +import co.paralleluniverse.fibers.Suspendable +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +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.serialization.CordaSerializable +import net.corda.core.utilities.ProgressTracker +import net.corda.core.utilities.unwrap +import org.bouncycastle.cert.X509CertificateHolder +import java.security.cert.CertPath + +/** + * Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction. + * This is intended for use as a subflow of another flow. + */ +object TxKeyFlow { + abstract class AbstractIdentityFlow(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic() { + fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity { + val (certPath, theirCert, txIdentity) = untrustedIdentity + if (theirCert.subject == otherSide.name) { + serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath) + return AnonymousIdentity(certPath, theirCert, txIdentity) + } else + throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}") + } + } + + @StartableByRPC + @InitiatingFlow + class Requester(otherSide: Party, + override val progressTracker: ProgressTracker) : AbstractIdentityFlow>(otherSide, false) { + constructor(otherSide: Party) : this(otherSide, tracker()) + companion object { + object AWAITING_KEY : ProgressTracker.Step("Awaiting key") + + fun tracker() = ProgressTracker(AWAITING_KEY) + } + + @Suspendable + override fun call(): Map { + progressTracker.currentStep = AWAITING_KEY + val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + val myIdentity = AnonymousIdentity(myIdentityFragment) + val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } + send(otherSide, myIdentity) + return mapOf(Pair(otherSide, myIdentity), + Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + } + } + + /** + * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the + * counterparty and as the result from the flow. + */ + @InitiatedBy(Requester::class) + class Provider(otherSide: Party) : AbstractIdentityFlow>(otherSide, false) { + companion object { + object SENDING_KEY : ProgressTracker.Step("Sending key") + } + + override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY) + + @Suspendable + override fun call(): Map { + val revocationEnabled = false + progressTracker.currentStep = SENDING_KEY + val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled) + val myIdentity = AnonymousIdentity(myIdentityFragment) + send(otherSide, myIdentity) + val theirIdentity = receive(otherSide).unwrap { validateIdentity(it) } + return mapOf(Pair(otherSide, myIdentity), + Pair(serviceHub.myInfo.legalIdentity, theirIdentity)) + } + } + + @CordaSerializable + data class AnonymousIdentity( + val certPath: CertPath, + val certificate: X509CertificateHolder, + val identity: AnonymousParty) { + constructor(myIdentity: Pair) : this(myIdentity.second, + myIdentity.first, + AnonymousParty(myIdentity.second.certificates.first().publicKey)) + } +} diff --git a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt b/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt deleted file mode 100644 index 8e7d85b240..0000000000 --- a/core/src/main/kotlin/net/corda/flows/TxKeyFlowUtilities.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.corda.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party -import net.corda.core.serialization.CordaSerializable -import net.corda.core.utilities.unwrap -import java.security.PublicKey -import java.security.cert.Certificate - -object TxKeyFlowUtilities { - /** - * Receive a key from a counterparty. This would normally be triggered by a flow as part of a transaction assembly - * process. - */ - @Suspendable - fun receiveKey(flow: FlowLogic<*>, otherSide: Party): Pair { - val untrustedKey = flow.receive(otherSide) - return untrustedKey.unwrap { - // TODO: Verify the certificate connects the given key to the counterparty, once we have certificates - Pair(it.key, it.certificate) - } - } - - /** - * Generates a new key and then returns it to the counterparty and as the result from the function. Note that this - * is an expensive operation, and should only be called once the calling flow has confirmed it wants to be part of - * a transaction with the counterparty, in order to avoid a DoS risk. - */ - @Suspendable - fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey { - 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)) - return key - } - - @CordaSerializable - data class ProvidedTransactionKey(val key: PublicKey, val certificate: Certificate?) -} diff --git a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java index 7f2d31ccaa..831d27549e 100644 --- a/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java +++ b/core/src/test/java/net/corda/core/flows/FlowsInJavaTest.java @@ -1,42 +1,43 @@ package net.corda.core.flows; -import co.paralleluniverse.fibers.*; +import co.paralleluniverse.fibers.Suspendable; import net.corda.core.identity.Party; -import net.corda.testing.node.*; -import org.junit.*; +import net.corda.testing.node.MockNetwork; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; -import java.util.concurrent.*; +import java.util.concurrent.Future; -import static org.assertj.core.api.AssertionsForClassTypes.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; public class FlowsInJavaTest { - private final MockNetwork net = new MockNetwork(); + private final MockNetwork mockNet = new MockNetwork(); private MockNetwork.MockNode node1; private MockNetwork.MockNode node2; @Before public void setUp() { - MockNetwork.BasketOfNodes someNodes = net.createSomeNodes(2); + MockNetwork.BasketOfNodes someNodes = mockNet.createSomeNodes(2); node1 = someNodes.getPartyNodes().get(0); node2 = someNodes.getPartyNodes().get(1); - net.runNetwork(); + mockNet.runNetwork(); } @After public void cleanUp() { - net.stopNodes(); + mockNet.stopNodes(); } @Test public void suspendableActionInsideUnwrap() throws Exception { - node2.getServices().registerServiceFlow(SendInUnwrapFlow.class, (otherParty) -> new OtherFlow(otherParty, "Hello")); + node2.registerInitiatedFlow(SendHelloAndThenReceive.class); Future result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture(); - net.runNetwork(); + mockNet.runNetwork(); assertThat(result.get()).isEqualTo("Hello"); } - @SuppressWarnings("unused") @InitiatingFlow private static class SendInUnwrapFlow extends FlowLogic { private final Party otherParty; @@ -55,19 +56,18 @@ public class FlowsInJavaTest { } } - private static class OtherFlow extends FlowLogic { + @InitiatedBy(SendInUnwrapFlow.class) + private static class SendHelloAndThenReceive extends FlowLogic { private final Party otherParty; - private final String payload; - private OtherFlow(Party otherParty, String payload) { + private SendHelloAndThenReceive(Party otherParty) { this.otherParty = otherParty; - this.payload = payload; } @Suspendable @Override public String call() throws FlowException { - return sendAndReceive(String.class, otherParty, payload).unwrap(data -> data); + return sendAndReceive(String.class, otherParty, "Hello").unwrap(data -> data); } } diff --git a/core/src/test/kotlin/net/corda/core/StreamsTest.kt b/core/src/test/kotlin/net/corda/core/StreamsTest.kt new file mode 100644 index 0000000000..f9b5ebc1ec --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/StreamsTest.kt @@ -0,0 +1,42 @@ +package net.corda.core + +import org.junit.Assert.assertArrayEquals +import org.junit.Test +import java.util.stream.IntStream +import java.util.stream.Stream +import kotlin.test.assertEquals + +class StreamsTest { + @Test + fun `IntProgression stream works`() { + assertArrayEquals(intArrayOf(1, 2, 3, 4), (1..4).stream().toArray()) + assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray()) + assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray()) + assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray()) + assertArrayEquals(intArrayOf(), (1..0).stream().toArray()) + assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray()) + assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray()) + assertArrayEquals(intArrayOf(3, 1), (3 downTo 1 step 2).stream().toArray()) + } + + @Test + fun `IntProgression spliterator characteristics and comparator`() { + val rangeCharacteristics = IntStream.range(0, 2).spliterator().characteristics() + val forward = (0..9 step 3).stream().spliterator() + assertEquals(rangeCharacteristics, forward.characteristics()) + assertEquals(null, forward.comparator) + val reverse = (9 downTo 0 step 3).stream().spliterator() + assertEquals(rangeCharacteristics, reverse.characteristics()) + assertEquals(Comparator.reverseOrder(), reverse.comparator) + } + + @Test + fun `Stream toTypedArray works`() { + val a: Array = Stream.of("one", "two").toTypedArray() + assertEquals(Array::class.java, a.javaClass) + assertArrayEquals(arrayOf("one", "two"), a) + val b: Array = Stream.of("one", "two", null).toTypedArray() + assertEquals(Array::class.java, b.javaClass) + assertArrayEquals(arrayOf("one", "two", null), b) + } +} diff --git a/core/src/test/kotlin/net/corda/core/UtilsTest.kt b/core/src/test/kotlin/net/corda/core/UtilsTest.kt index 7988d7eadc..d785102d07 100644 --- a/core/src/test/kotlin/net/corda/core/UtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/UtilsTest.kt @@ -1,10 +1,18 @@ package net.corda.core +import com.google.common.util.concurrent.MoreExecutors +import com.nhaarman.mockito_kotlin.mock +import com.nhaarman.mockito_kotlin.same +import com.nhaarman.mockito_kotlin.verify import org.assertj.core.api.Assertions.* import org.junit.Test +import org.mockito.ArgumentMatchers.anyString +import org.slf4j.Logger import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.CancellationException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit class UtilsTest { @Test @@ -57,4 +65,17 @@ class UtilsTest { future.get() } } -} \ No newline at end of file + + @Test + fun `andForget works`() { + val log = mock() + val throwable = Exception("Boom") + val executor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor()) + executor.submit { throw throwable }.andForget(log) + executor.shutdown() + while (!executor.awaitTermination(1, TimeUnit.SECONDS)) { + // Do nothing. + } + verify(log).error(anyString(), same(throwable)) + } +} diff --git a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt index fc59b89c96..4c53791c8f 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/AmountTests.kt @@ -20,6 +20,12 @@ class AmountTests { assertEquals(expected, amount.quantity) } + @Test + fun `make sure Amount has decimal places`() { + val x = Amount(1, Currency.getInstance("USD")) + assertTrue("0.01" in x.toString()) + } + @Test fun decimalConversion() { val quantity = 1234L diff --git a/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt b/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt index be9314793b..b1d915a745 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/StructuresTests.kt @@ -6,10 +6,12 @@ import com.nhaarman.mockito_kotlin.whenever import org.junit.Test import java.io.ByteArrayOutputStream import java.io.IOException +import java.util.* import java.util.jar.JarFile.MANIFEST_NAME import java.util.zip.ZipEntry import java.util.zip.ZipOutputStream import kotlin.test.assertEquals +import kotlin.test.assertNotEquals import kotlin.test.fail class AttachmentTest { @@ -39,3 +41,35 @@ class AttachmentTest { } } + +class UniqueIdentifierTests { + + @Test + fun `unique identifier comparison`() { + val ids = listOf(UniqueIdentifier.fromString("e363f00e-4759-494d-a7ca-0dc966a92494"), + UniqueIdentifier.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d"), + UniqueIdentifier("Test", UUID.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d")) + ) + assertEquals(-1, ids[0].compareTo(ids[1])) + assertEquals(1, ids[1].compareTo(ids[0])) + assertEquals(0, ids[0].compareTo(ids[0])) + // External ID is not taken into account + assertEquals(0, ids[1].compareTo(ids[2])) + } + + @Test + fun `unique identifier equality`() { + val ids = listOf(UniqueIdentifier.fromString("e363f00e-4759-494d-a7ca-0dc966a92494"), + UniqueIdentifier.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d"), + UniqueIdentifier("Test", UUID.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d")) + ) + assertEquals(ids[0], ids[0]) + assertNotEquals(ids[0], ids[1]) + assertEquals(ids[0].hashCode(), ids[0].hashCode()) + assertNotEquals(ids[0].hashCode(), ids[1].hashCode()) + // External ID is not taken into account + assertEquals(ids[1], ids[2]) + assertEquals(ids[1].hashCode(), ids[2].hashCode()) + } + +} \ No newline at end of file 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 8fd3bad22a..c13ebbae09 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionEncumbranceTests.kt @@ -30,7 +30,7 @@ class TransactionEncumbranceTests { override val legalContractReference = SecureHash.sha256("DummyTimeLock") override fun verify(tx: TransactionForContract) { val timeLockInput = tx.inputs.filterIsInstance().singleOrNull() ?: return - val time = tx.timestamp?.before ?: throw IllegalArgumentException("Transactions containing time-locks must be timestamped") + val time = tx.timeWindow?.untilTime ?: throw IllegalArgumentException("Transactions containing time-locks must have a time-window") requireThat { "the time specified in the time-lock has passed" using (time >= timeLockInput.validFrom) } @@ -70,7 +70,7 @@ class TransactionEncumbranceTests { input("5pm time-lock") output { stateWithNewOwner } command(MEGA_CORP.owningKey) { Cash.Commands.Move() } - timestamp(FIVE_PM) + timeWindow(FIVE_PM) verifies() } } @@ -89,7 +89,7 @@ class TransactionEncumbranceTests { input("5pm time-lock") output { state } command(MEGA_CORP.owningKey) { Cash.Commands.Move() } - timestamp(FOUR_PM) + timeWindow(FOUR_PM) this `fails with` "the time specified in the time-lock has passed" } } @@ -106,7 +106,7 @@ class TransactionEncumbranceTests { input("state encumbered by 5pm time-lock") output { stateWithNewOwner } command(MEGA_CORP.owningKey) { Cash.Commands.Move() } - timestamp(FIVE_PM) + timeWindow(FIVE_PM) this `fails with` "Missing required encumbrance 1 in INPUT" } } @@ -146,7 +146,7 @@ class TransactionEncumbranceTests { input("5pm time-lock") output { stateWithNewOwner } command(MEGA_CORP.owningKey) { Cash.Commands.Move() } - timestamp(FIVE_PM) + timeWindow(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 05b2e39479..61750d7619 100644 --- a/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt +++ b/core/src/test/kotlin/net/corda/core/contracts/TransactionTests.kt @@ -41,7 +41,7 @@ class TransactionTests { notary = DUMMY_NOTARY, signers = listOf(compKey, DUMMY_KEY_1.public, DUMMY_KEY_2.public), type = TransactionType.General, - timestamp = null + timeWindow = null ) assertEquals( setOf(compKey, DUMMY_KEY_2.public), @@ -69,7 +69,7 @@ class TransactionTests { notary = DUMMY_NOTARY, signers = listOf(DUMMY_KEY_1.public, DUMMY_KEY_2.public), type = TransactionType.General, - timestamp = null + timeWindow = null ) assertFailsWith { makeSigned(wtx).verifySignatures() } @@ -101,7 +101,7 @@ class TransactionTests { val attachments = emptyList() val id = SecureHash.randomSHA256() val signers = listOf(DUMMY_NOTARY_KEY.public) - val timestamp: Timestamp? = null + val timeWindow: TimeWindow? = null val transaction: LedgerTransaction = LedgerTransaction( inputs, outputs, @@ -110,7 +110,7 @@ class TransactionTests { id, null, signers, - timestamp, + timeWindow, TransactionType.General ) @@ -128,7 +128,7 @@ class TransactionTests { val attachments = emptyList() val id = SecureHash.randomSHA256() val signers = listOf(DUMMY_NOTARY_KEY.public) - val timestamp: Timestamp? = null + val timeWindow: TimeWindow? = null val transaction: LedgerTransaction = LedgerTransaction( inputs, outputs, @@ -137,7 +137,7 @@ class TransactionTests { id, DUMMY_NOTARY, signers, - timestamp, + timeWindow, TransactionType.General ) @@ -155,7 +155,7 @@ class TransactionTests { val attachments = emptyList() val id = SecureHash.randomSHA256() val signers = listOf(DUMMY_NOTARY_KEY.public) - val timestamp: Timestamp? = null + val timeWindow: TimeWindow? = null val transaction: LedgerTransaction = LedgerTransaction( inputs, outputs, @@ -164,7 +164,7 @@ class TransactionTests { id, notary, signers, - timestamp, + timeWindow, TransactionType.General ) diff --git a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt index 5cf97d4a98..62508c42df 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt @@ -2,6 +2,7 @@ package net.corda.core.crypto import com.google.common.collect.Sets import net.i2p.crypto.eddsa.EdDSAKey +import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec @@ -9,6 +10,7 @@ import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.interfaces.ECKey @@ -640,9 +642,9 @@ class CryptoUtilsTest { val keyPairEdDSA = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) val pubEdDSA = keyPairEdDSA.public assertTrue(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, pubEdDSA)) - // use R1 curve for check. + // Use R1 curve for check. assertFalse(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, pubEdDSA)) - // check for point at infinity. + // Check for point at infinity. val pubKeySpec = EdDSAPublicKeySpec((Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3), Crypto.EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec) assertFalse(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, EdDSAPublicKey(pubKeySpec))) } @@ -652,8 +654,131 @@ class CryptoUtilsTest { val keyGen = KeyPairGenerator.getInstance("EC") // sun.security.ec.ECPublicKeyImpl keyGen.initialize(256, newSecureRandom()) val pairSun = keyGen.generateKeyPair() - val pubSun = pairSun.getPublic() - // should fail as pubSun is not a BCECPublicKey. + val pubSun = pairSun.public + // Should fail as pubSun is not a BCECPublicKey. Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, pubSun) } + + @Test + fun `ECDSA secp256R1 deterministic key generation`() { + val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) + val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + + // Check scheme. + assertEquals(priv.algorithm, dpriv.algorithm) + assertEquals(pub.algorithm, dpub.algorithm) + assertTrue(dpriv is BCECPrivateKey) + assertTrue(dpub is BCECPublicKey) + assertEquals((dpriv as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) + assertEquals((dpub as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1")) + assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.ECDSA_SECP256R1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpub), Crypto.ECDSA_SECP256R1_SHA256) + + // Validate public key. + assertTrue(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256R1_SHA256, dpub)) + + // Try to sign/verify. + val signedData = Crypto.doSign(dpriv, testBytes) + val verification = Crypto.doVerify(dpub, signedData, testBytes) + assertTrue(verification) + + // Check it is a new keyPair. + assertNotEquals(priv, dpriv) + assertNotEquals(pub, dpub) + + // A new keyPair is always generated per different seed. + val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertNotEquals(dpriv, dpriv2) + assertNotEquals(dpub, dpub2) + + // Check if the same input always produces the same output (i.e. deterministically generated). + val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + assertEquals(dpriv, dpriv_1) + assertEquals(dpub, dpub_1) + val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertEquals(dpriv2, dpriv_2) + assertEquals(dpub2, dpub_2) + } + + @Test + fun `ECDSA secp256K1 deterministic key generation`() { + val (priv, pub) = Crypto.generateKeyPair(Crypto.ECDSA_SECP256K1_SHA256) + val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + + // Check scheme. + assertEquals(priv.algorithm, dpriv.algorithm) + assertEquals(pub.algorithm, dpub.algorithm) + assertTrue(dpriv is BCECPrivateKey) + assertTrue(dpub is BCECPublicKey) + assertEquals((dpriv as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) + assertEquals((dpub as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1")) + assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.ECDSA_SECP256K1_SHA256) + assertEquals(Crypto.findSignatureScheme(dpub), Crypto.ECDSA_SECP256K1_SHA256) + + // Validate public key. + assertTrue(Crypto.publicKeyOnCurve(Crypto.ECDSA_SECP256K1_SHA256, dpub)) + + // Try to sign/verify. + val signedData = Crypto.doSign(dpriv, testBytes) + val verification = Crypto.doVerify(dpub, signedData, testBytes) + assertTrue(verification) + + // check it is a new keyPair. + assertNotEquals(priv, dpriv) + assertNotEquals(pub, dpub) + + // A new keyPair is always generated per different seed. + val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertNotEquals(dpriv, dpriv2) + assertNotEquals(dpub, dpub2) + + // Check if the same input always produces the same output (i.e. deterministically generated). + val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + assertEquals(dpriv, dpriv_1) + assertEquals(dpub, dpub_1) + val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertEquals(dpriv2, dpriv_2) + assertEquals(dpub2, dpub_2) + } + + @Test + fun `EdDSA ed25519 deterministic key generation`() { + val (priv, pub) = Crypto.generateKeyPair(Crypto.EDDSA_ED25519_SHA512) + val (dpriv, dpub) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + + // Check scheme. + assertEquals(priv.algorithm, dpriv.algorithm) + assertEquals(pub.algorithm, dpub.algorithm) + assertTrue(dpriv is EdDSAPrivateKey) + assertTrue(dpub is EdDSAPublicKey) + assertEquals((dpriv as EdDSAKey).params, EdDSANamedCurveTable.getByName("ED25519")) + assertEquals((dpub as EdDSAKey).params, EdDSANamedCurveTable.getByName("ED25519")) + assertEquals(Crypto.findSignatureScheme(dpriv), Crypto.EDDSA_ED25519_SHA512) + assertEquals(Crypto.findSignatureScheme(dpub), Crypto.EDDSA_ED25519_SHA512) + + // Validate public key. + assertTrue(Crypto.publicKeyOnCurve(Crypto.EDDSA_ED25519_SHA512, dpub)) + + // Try to sign/verify. + val signedData = Crypto.doSign(dpriv, testBytes) + val verification = Crypto.doVerify(dpub, signedData, testBytes) + assertTrue(verification) + + // Check it is a new keyPair. + assertNotEquals(priv, dpriv) + assertNotEquals(pub, dpub) + + // A new keyPair is always generated per different seed. + val (dpriv2, dpub2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertNotEquals(dpriv, dpriv2) + assertNotEquals(dpub, dpub2) + + // Check if the same input always produces the same output (i.e. deterministically generated). + val (dpriv_1, dpub_1) = Crypto.deterministicKeyPair(priv, "seed-1".toByteArray()) + assertEquals(dpriv, dpriv_1) + assertEquals(dpub, dpub_1) + val (dpriv_2, dpub_2) = Crypto.deterministicKeyPair(priv, "seed-2".toByteArray()) + assertEquals(dpriv2, dpriv_2) + assertEquals(dpub2, dpub_2) + } } 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 dbbc52c5e0..2452fc8466 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/PartialMerkleTreeTest.kt @@ -15,6 +15,7 @@ import net.corda.core.utilities.TEST_TX_TIME import net.corda.testing.* import org.junit.Test import java.security.PublicKey +import java.util.function.Predicate import kotlin.test.* class PartialMerkleTreeTest { @@ -43,7 +44,7 @@ class PartialMerkleTreeTest { input("MEGA_CORP cash") output("MEGA_CORP cash".output().copy(owner = MINI_CORP)) command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } } @@ -98,13 +99,13 @@ class PartialMerkleTreeTest { is StateRef -> true is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys is Command -> MEGA_CORP_PUBKEY in elem.signers - is Timestamp -> true + is TimeWindow -> true is PublicKey -> elem == MEGA_CORP_PUBKEY else -> false } } - val mt = testTx.buildFilteredTransaction(::filtering) + val mt = testTx.buildFilteredTransaction(Predicate(::filtering)) val leaves = mt.filteredLeaves val d = WireTransaction.deserialize(testTx.serialized) assertEquals(testTx.id, d.id) @@ -113,7 +114,7 @@ class PartialMerkleTreeTest { assertEquals(1, leaves.inputs.size) assertEquals(1, leaves.mustSign.size) assertEquals(0, leaves.attachments.size) - assertTrue(mt.filteredLeaves.timestamp != null) + assertTrue(mt.filteredLeaves.timeWindow != null) assertEquals(null, mt.filteredLeaves.type) assertEquals(null, mt.filteredLeaves.notary) assertTrue(mt.verify()) @@ -128,12 +129,12 @@ class PartialMerkleTreeTest { @Test fun `nothing filtered`() { - val mt = testTx.buildFilteredTransaction({ false }) + val mt = testTx.buildFilteredTransaction(Predicate { false }) assertTrue(mt.filteredLeaves.attachments.isEmpty()) assertTrue(mt.filteredLeaves.commands.isEmpty()) assertTrue(mt.filteredLeaves.inputs.isEmpty()) assertTrue(mt.filteredLeaves.outputs.isEmpty()) - assertTrue(mt.filteredLeaves.timestamp == null) + assertTrue(mt.filteredLeaves.timeWindow == null) assertFailsWith { mt.verify() } } @@ -219,7 +220,7 @@ class PartialMerkleTreeTest { } } - private fun makeSimpleCashWtx(notary: Party, timestamp: Timestamp? = null, attachments: List = emptyList()): WireTransaction { + private fun makeSimpleCashWtx(notary: Party, timeWindow: TimeWindow? = null, attachments: List = emptyList()): WireTransaction { return WireTransaction( inputs = testTx.inputs, attachments = attachments, @@ -228,7 +229,7 @@ class PartialMerkleTreeTest { notary = notary, signers = listOf(MEGA_CORP_PUBKEY, DUMMY_PUBKEY_1), type = TransactionType.General, - timestamp = timestamp + timeWindow = timeWindow ) } } diff --git a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt index 2d687df267..30292b00e7 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509NameConstraintsTest.kt @@ -1,5 +1,6 @@ package net.corda.core.crypto +import net.corda.core.toTypedArray import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree @@ -7,10 +8,8 @@ import org.bouncycastle.asn1.x509.NameConstraints import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test import java.security.KeyStore -import java.security.cert.CertPathValidator -import java.security.cert.CertPathValidatorException -import java.security.cert.CertificateFactory -import java.security.cert.PKIXParameters +import java.security.cert.* +import java.util.stream.Stream import kotlin.test.assertFailsWith import kotlin.test.assertTrue @@ -18,25 +17,26 @@ class X509NameConstraintsTest { private fun makeKeyStores(subjectName: X500Name, nameConstraints: NameConstraints): Pair { val rootKeys = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Root CA"), rootKeys) + val rootCACert = X509Utilities.createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Root CA","London","demo@r3.com",null), rootKeys) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getDevX509Name("Corda Intermediate CA"), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootKeys, X509Utilities.getX509Name("Corda Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public) val clientCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getDevX509Name("Corda Client CA"), clientCAKeyPair.public, nameConstraints = nameConstraints) + val clientCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, intermediateCACert, intermediateCAKeyPair, X509Utilities.getX509Name("Corda Client CA","London","demo@r3.com",null), clientCAKeyPair.public, nameConstraints = nameConstraints) val keyPass = "password" val trustStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) trustStore.load(null, keyPass.toCharArray()) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert) val tlsKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val tlsCert = X509Utilities.createCertificate(CertificateType.TLS, clientCACert, clientCAKeyPair, subjectName, tlsKey.public) val keyStore = KeyStore.getInstance(KeyStoreUtilities.KEYSTORE_TYPE) keyStore.load(null, keyPass.toCharArray()) - keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(), arrayOf(tlsCert, clientCACert, intermediateCACert, rootCACert)) + keyStore.addOrReplaceKey(X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass.toCharArray(), + Stream.of(tlsCert, clientCACert, intermediateCACert, rootCACert).map { it.cert }.toTypedArray()) return Pair(keyStore, trustStore) } @@ -103,7 +103,7 @@ class X509NameConstraintsTest { } assertTrue { - val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, UID=, E=me@email.com, C=UK"), nameConstraints) + val (keystore, trustStore) = makeKeyStores(X500Name("CN=Bank A TLS, UID=, E=me@email.com, C=GB"), nameConstraints) val params = PKIXParameters(trustStore) params.isRevocationEnabled = false val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) @@ -112,7 +112,7 @@ class X509NameConstraintsTest { } assertTrue { - val (keystore, trustStore) = makeKeyStores(X500Name("O=Bank A, UID=, E=me@email.com, C=UK"), nameConstraints) + val (keystore, trustStore) = makeKeyStores(X500Name("O=Bank A, UID=, E=me@email.com, C=GB"), nameConstraints) val params = PKIXParameters(trustStore) params.isRevocationEnabled = false val certPath = certFactory.generateCertPath(keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_TLS).asList()) @@ -121,4 +121,4 @@ class X509NameConstraintsTest { } } -} \ No newline at end of file +} 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 2a25d8c60f..780c300044 100644 --- a/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt +++ b/core/src/test/kotlin/net/corda/core/crypto/X509UtilitiesTest.kt @@ -5,9 +5,14 @@ import net.corda.core.crypto.Crypto.generateKeyPair import net.corda.core.crypto.X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME import net.corda.core.crypto.X509Utilities.createSelfSignedCACertificate import net.corda.core.div +import net.corda.core.toTypedArray import net.corda.testing.MEGA_CORP import net.corda.testing.getTestX509Name import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.KeyUsage +import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder import org.junit.Rule import org.junit.Test import org.junit.rules.TemporaryFolder @@ -20,14 +25,13 @@ import java.nio.file.Path import java.security.KeyStore import java.security.PrivateKey import java.security.SecureRandom +import java.security.cert.Certificate import java.security.cert.X509Certificate import java.util.* +import java.util.stream.Stream import javax.net.ssl.* import kotlin.concurrent.thread -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertNotNull -import kotlin.test.assertTrue +import kotlin.test.* class X509UtilitiesTest { @Rule @@ -38,12 +42,14 @@ class X509UtilitiesTest { fun `create valid self-signed CA certificate`() { val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = createSelfSignedCACertificate(getTestX509Name("Test Cert"), caKey) - assertTrue { caCert.subjectDN.name.contains("CN=Test Cert") } // using our subject common name - assertEquals(caCert.issuerDN, caCert.subjectDN) //self-signed - caCert.checkValidity(Date()) // throws on verification problems - caCert.verify(caKey.public) // throws on verification problems - assertTrue { caCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) - assertTrue { caCert.basicConstraints > 0 } // This returns the signing path length Would be -1 for non-CA certificate + assertTrue { caCert.subject.commonName == "Test Cert" } // using our subject common name + assertEquals(caCert.issuer, caCert.subject) //self-signed + caCert.isValidOn(Date()) // throws on verification problems + caCert.isSignatureValid(JcaContentVerifierProviderBuilder().build(caKey.public)) // throws on verification problems + val basicConstraints = BasicConstraints.getInstance(caCert.getExtension(Extension.basicConstraints).parsedValue) + val keyUsage = KeyUsage.getInstance(caCert.getExtension(Extension.keyUsage).parsedValue) + assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) + assertNull(basicConstraints.pathLenConstraint) // No length constraint specified on this CA certificate } @Test @@ -60,15 +66,17 @@ class X509UtilitiesTest { fun `create valid server certificate chain`() { val caKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) val caCert = createSelfSignedCACertificate(getTestX509Name("Test CA Cert"), caKey) - val subjectDN = getTestX509Name("Server Cert") + val subject = getTestX509Name("Server Cert") val keyPair = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subjectDN, keyPair.public) - assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name - assertEquals(caCert.issuerDN, serverCert.issuerDN) // Issued by our CA cert - serverCert.checkValidity(Date()) // throws on verification problems - serverCert.verify(caKey.public) // throws on verification problems - assertFalse { serverCert.keyUsage[5] } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) - assertTrue { serverCert.basicConstraints == -1 } // This returns the signing path length should be -1 for non-CA certificate + val serverCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, caKey, subject, keyPair.public) + assertTrue { serverCert.subject.toString().contains("CN=Server Cert") } // using our subject common name + assertEquals(caCert.issuer, serverCert.issuer) // Issued by our CA cert + serverCert.isValidOn(Date()) // throws on verification problems + serverCert.isSignatureValid(JcaContentVerifierProviderBuilder().build(caKey.public)) // throws on verification problems + val basicConstraints = BasicConstraints.getInstance(serverCert.getExtension(Extension.basicConstraints).parsedValue) + val keyUsage = KeyUsage.getInstance(serverCert.getExtension(Extension.keyUsage).parsedValue) + assertFalse { keyUsage.hasUsages(5) } // Bit 5 == keyCertSign according to ASN.1 spec (see full comment on KeyUsage property) + assertNull(basicConstraints.pathLenConstraint) // Non-CA certificate } @Test @@ -78,11 +86,12 @@ class X509UtilitiesTest { val keyPair = generateKeyPair(EDDSA_ED25519_SHA512) val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) - assertEquals(selfSignCert.publicKey, keyPair.public) + assertTrue(Arrays.equals(selfSignCert.subjectPublicKeyInfo.encoded, keyPair.public.encoded)) // Save the EdDSA private key with self sign cert in the keystore. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), arrayOf(selfSignCert)) + keyStore.setKeyEntry("Key", keyPair.private, "password".toCharArray(), + Stream.of(selfSignCert).map { it.cert }.toTypedArray()) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -106,7 +115,8 @@ class X509UtilitiesTest { // Save the EdDSA private key with cert chains. val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") - keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), arrayOf(ecDSACert, edDSACert)) + keyStore.setKeyEntry("Key", edDSAKeypair.private, "password".toCharArray(), + Stream.of(ecDSACert, edDSACert).map { it.cert }.toTypedArray()) keyStore.save(tmpKeyStore, "keystorepass") // Load the keystore from file and make sure keys are intact. @@ -182,23 +192,24 @@ class X509UtilitiesTest { val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, "serverkeypass") - serverCertAndKey.certificate.checkValidity(Date()) - serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) + serverCertAndKey.certificate.isValidOn(Date()) + serverCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(caCertAndKey.certificate.subjectPublicKeyInfo)) - assertTrue { serverCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } + assertTrue { serverCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) } // Load back server certificate val sslKeyStore = KeyStoreUtilities.loadKeyStore(tmpSSLKeyStore, "serverstorepass") val sslCertAndKey = sslKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_TLS, "serverkeypass") - sslCertAndKey.certificate.checkValidity(Date()) - sslCertAndKey.certificate.verify(serverCertAndKey.certificate.publicKey) + sslCertAndKey.certificate.isValidOn(Date()) + sslCertAndKey.certificate.isSignatureValid(JcaContentVerifierProviderBuilder().build(serverCertAndKey.certificate.subjectPublicKeyInfo)) - assertTrue { sslCertAndKey.certificate.subjectDN.name.contains(MEGA_CORP.name.commonName) } + assertTrue { sslCertAndKey.certificate.subject.toString().contains(MEGA_CORP.name.commonName) } // Now sign something with private key and verify against certificate public key val testData = "123456".toByteArray() val signature = Crypto.doSign(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.keyPair.private, testData) - assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, serverCertAndKey.certificate.publicKey, signature, testData) } + val publicKey = Crypto.toSupportedPublicKey(serverCertAndKey.certificate.subjectPublicKeyInfo) + assertTrue { Crypto.isValid(DEFAULT_TLS_SIGNATURE_SCHEME, publicKey, signature, testData) } } @Test @@ -331,27 +342,27 @@ class X509UtilitiesTest { trustStorePassword: String ): KeyStore { val rootCAKey = generateKeyPair(DEFAULT_TLS_SIGNATURE_SCHEME) - val rootCACert = createSelfSignedCACertificate(X509Utilities.getDevX509Name("Corda Node Root CA"), rootCAKey) + val rootCACert = createSelfSignedCACertificate(X509Utilities.getX509Name("Corda Node Root CA","London","demo@r3.com",null), rootCAKey) val intermediateCAKeyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getDevX509Name("Corda Node Intermediate CA"), intermediateCAKeyPair.public) + val intermediateCACert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, rootCACert, rootCAKey, X509Utilities.getX509Name("Corda Node Intermediate CA","London","demo@r3.com",null), intermediateCAKeyPair.public) val keyPass = keyPassword.toCharArray() val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) - keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf(rootCACert)) + keyStore.addOrReplaceKey(X509Utilities.CORDA_ROOT_CA, rootCAKey.private, keyPass, arrayOf(rootCACert.cert)) keyStore.addOrReplaceKey(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCAKeyPair.private, keyPass, - arrayOf(intermediateCACert, rootCACert)) + Stream.of(intermediateCACert, rootCACert).map { it.cert }.toTypedArray()) keyStore.save(keyStoreFilePath, storePassword) val trustStore = KeyStoreUtilities.loadOrCreateKeyStore(trustStoreFilePath, trustStorePassword) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert) - trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_ROOT_CA, rootCACert.cert) + trustStore.addOrReplaceCertificate(X509Utilities.CORDA_INTERMEDIATE_CA, intermediateCACert.cert) trustStore.save(trustStoreFilePath, trustStorePassword) @@ -363,7 +374,7 @@ class X509UtilitiesTest { val keyPair = generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256) val selfSignCert = createSelfSignedCACertificate(X500Name("CN=Test"), keyPair) val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword") - keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert)) + keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.cert)) val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray()) val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword") diff --git a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt index 7e30609ca7..1396799b8c 100644 --- a/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt +++ b/core/src/test/kotlin/net/corda/core/flows/CollectSignaturesFlowTests.kt @@ -7,7 +7,6 @@ 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 @@ -18,7 +17,7 @@ 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.reflect.KClass import kotlin.test.assertFailsWith class CollectSignaturesFlowTests { @@ -37,9 +36,6 @@ class CollectSignaturesFlowTests { c = nodes.partyNodes[2] notary = nodes.notaryNode.info.notaryIdentity mockNet.runNetwork() - CollectSigsTestCorDapp.registerFlows(a.services) - CollectSigsTestCorDapp.registerFlows(b.services) - CollectSigsTestCorDapp.registerFlows(c.services) } @After @@ -47,11 +43,9 @@ class CollectSignaturesFlowTests { 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) } + private fun registerFlowOnAllNodes(flowClass: KClass>) { + listOf(a, b, c).forEach { + it.registerInitiatedFlow(flowClass.java) } } @@ -82,6 +76,7 @@ class CollectSignaturesFlowTests { } } + @InitiatedBy(TestFlow.Initiator::class) class Responder(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { @@ -104,7 +99,7 @@ class CollectSignaturesFlowTests { // receiving off the wire. object TestFlowTwo { @InitiatingFlow - class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic() { + class Initiator(val state: DummyContract.MultiOwnerState) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity @@ -118,6 +113,7 @@ class CollectSignaturesFlowTests { } } + @InitiatedBy(TestFlowTwo.Initiator::class) class Responder(val otherParty: Party) : FlowLogic() { @Suspendable override fun call(): SignedTransaction { val flow = object : SignTransactionFlow(otherParty) { @@ -137,13 +133,13 @@ class CollectSignaturesFlowTests { } } - @Test fun `successfully collects two signatures`() { + registerFlowOnAllNodes(TestFlowTwo.Responder::class) 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)) + val flow = a.services.startFlow(TestFlowTwo.Initiator(state)) mockNet.runNetwork() val result = flow.resultFuture.getOrThrow() result.verifySignatures() @@ -169,8 +165,8 @@ class CollectSignaturesFlowTests { 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() + assertFailsWith("The Initiator of CollectSignaturesFlow must have signed the transaction.") { + flow.resultFuture.getOrThrow() } } 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 4e71b4d222..d0335f7c44 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ContractUpgradeFlowTest.kt @@ -175,12 +175,13 @@ class ContractUpgradeFlowTest { // Create some cash. val result = a.services.startFlow(CashIssueFlow(Amount(1000, USD), OpaqueBytes.of(1), a.info.legalIdentity, notary)).resultFuture mockNet.runNetwork() + val stateAndRef = result.getOrThrow().tx.outRef(0) val baseState = a.database.transaction { a.vault.unconsumedStates().single() } assertTrue(baseState.state.data is Cash.State, "Contract state is old version.") - val stateAndRef = result.getOrThrow().tx.outRef(0) // Starts contract upgrade flow. - a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java)) + val upgradeResult = a.services.startFlow(ContractUpgradeFlow(stateAndRef, CashV2::class.java)).resultFuture mockNet.runNetwork() + upgradeResult.getOrThrow() // Get contract state from the vault. val firstState = a.database.transaction { a.vault.unconsumedStates().single() } assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") 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 a48e72944f..887e1649dc 100644 --- a/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt +++ b/core/src/test/kotlin/net/corda/core/flows/ResolveTransactionsFlowTest.kt @@ -28,24 +28,24 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull class ResolveTransactionsFlowTest { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var a: MockNetwork.MockNode lateinit var b: MockNetwork.MockNode lateinit var notary: Party @Before fun setup() { - net = MockNetwork() - val nodes = net.createSomeNodes() + mockNet = MockNetwork() + val nodes = mockNet.createSomeNodes() a = nodes.partyNodes[0] b = nodes.partyNodes[1] notary = nodes.notaryNode.info.notaryIdentity - net.runNetwork() + mockNet.runNetwork() } @After fun tearDown() { - net.stopNodes() + mockNet.stopNodes() } // DOCSTART 1 @@ -54,7 +54,7 @@ class ResolveTransactionsFlowTest { val (stx1, stx2) = makeTransactions() val p = ResolveTransactionsFlow(setOf(stx2.id), a.info.legalIdentity) val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() val results = future.getOrThrow() assertEquals(listOf(stx1.id, stx2.id), results.map { it.id }) b.database.transaction { @@ -69,7 +69,7 @@ class ResolveTransactionsFlowTest { val stx = makeTransactions(signFirstTX = false).second val p = ResolveTransactionsFlow(setOf(stx.id), a.info.legalIdentity) val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith(SignatureException::class) { future.getOrThrow() } } @@ -78,7 +78,7 @@ class ResolveTransactionsFlowTest { val (stx1, stx2) = makeTransactions() val p = ResolveTransactionsFlow(stx2, a.info.legalIdentity) val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() b.database.transaction { assertEquals(stx1, b.storage.validatedTransactions.getTransaction(stx1.id)) @@ -105,7 +105,7 @@ class ResolveTransactionsFlowTest { val p = ResolveTransactionsFlow(setOf(cursor.id), a.info.legalIdentity) p.transactionCountLimit = 40 val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith { future.getOrThrow() } } @@ -131,7 +131,7 @@ class ResolveTransactionsFlowTest { val p = ResolveTransactionsFlow(setOf(stx3.id), a.info.legalIdentity) val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() } @@ -153,7 +153,7 @@ class ResolveTransactionsFlowTest { val stx2 = makeTransactions(withAttachment = id).second val p = ResolveTransactionsFlow(stx2, a.info.legalIdentity) val future = b.services.startFlow(p).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() // TODO: this operation should not require an explicit transaction diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt deleted file mode 100644 index 78f18c3171..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlow.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.corda.core.flows - -import co.paralleluniverse.fibers.Suspendable -import net.corda.core.identity.Party -import net.corda.core.utilities.ProgressTracker -import net.corda.flows.TxKeyFlowUtilities -import java.security.PublicKey -import java.security.cert.Certificate - -/** - * Very basic flow which requests a transaction key from a counterparty, used for testing [TxKeyFlowUtilities]. - * This MUST not be provided on any real node, as the ability for arbitrary parties to request keys would enable - * DoS of the node, as key generation/storage is vastly more expensive than submitting a request. - */ -object TxKeyFlow { - - @InitiatingFlow - class Requester(val otherSide: Party, - override val progressTracker: ProgressTracker) : FlowLogic>() { - constructor(otherSide: Party) : this(otherSide, tracker()) - - companion object { - object AWAITING_KEY : ProgressTracker.Step("Awaiting key") - - fun tracker() = ProgressTracker(AWAITING_KEY) - } - - @Suspendable - override fun call(): Pair { - progressTracker.currentStep = AWAITING_KEY - return TxKeyFlowUtilities.receiveKey(this, otherSide) - } - } - - /** - * Flow which waits for a key request from a counterparty, generates a new key and then returns it to the - * counterparty and as the result from the flow. - */ - class Provider(val otherSide: Party, - override val progressTracker: ProgressTracker) : FlowLogic() { - constructor(otherSide: Party) : this(otherSide, tracker()) - - companion object { - object SENDING_KEY : ProgressTracker.Step("Sending key") - - fun tracker() = ProgressTracker(SENDING_KEY) - } - - @Suspendable - override fun call(): PublicKey { - progressTracker.currentStep = SENDING_KEY - return TxKeyFlowUtilities.provideKey(this, otherSide) - } - } -} diff --git a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt b/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt deleted file mode 100644 index 662a807269..0000000000 --- a/core/src/test/kotlin/net/corda/core/flows/TxKeyFlowUtilitiesTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.corda.core.flows - -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.node.MockNetwork -import org.junit.Before -import org.junit.Test -import java.security.PublicKey -import kotlin.test.assertNotNull - -class TxKeyFlowUtilitiesTests { - lateinit var net: MockNetwork - - @Before - fun before() { - net = MockNetwork(false) - } - - @Test - fun `issue key`() { - // We run this in parallel threads to help catch any race conditions that may exist. - net = MockNetwork(false, true) - - // Set up values we'll need - 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 bobKey: Party = bobNode.services.myInfo.legalIdentity - - // Run the flows - bobNode.registerServiceFlow(TxKeyFlow.Requester::class) { TxKeyFlow.Provider(it) } - val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bobKey)) - - // Get the results - val actual: PublicKey = requesterFlow.resultFuture.get().first - assertNotNull(actual) - } -} diff --git a/core/src/test/kotlin/net/corda/core/node/services/TimeWindowCheckerTests.kt b/core/src/test/kotlin/net/corda/core/node/services/TimeWindowCheckerTests.kt new file mode 100644 index 0000000000..9b174d6b41 --- /dev/null +++ b/core/src/test/kotlin/net/corda/core/node/services/TimeWindowCheckerTests.kt @@ -0,0 +1,33 @@ +package net.corda.core.node.services + +import net.corda.core.contracts.TimeWindow +import net.corda.core.seconds +import org.junit.Test +import java.time.Clock +import java.time.Instant +import java.time.ZoneId +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class TimeWindowCheckerTests { + val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()) + val timeWindowChecker = TimeWindowChecker(clock, tolerance = 30.seconds) + + @Test + fun `should return true for valid time-window`() { + val now = clock.instant() + val timeWindowPast = TimeWindow.between(now - 60.seconds, now - 29.seconds) + val timeWindowFuture = TimeWindow.between(now + 29.seconds, now + 60.seconds) + assertTrue { timeWindowChecker.isValid(timeWindowPast) } + assertTrue { timeWindowChecker.isValid(timeWindowFuture) } + } + + @Test + fun `should return false for invalid time-window`() { + val now = clock.instant() + val timeWindowPast = TimeWindow.between(now - 60.seconds, now - 31.seconds) + val timeWindowFuture = TimeWindow.between(now + 31.seconds, now + 60.seconds) + assertFalse { timeWindowChecker.isValid(timeWindowPast) } + assertFalse { timeWindowChecker.isValid(timeWindowFuture) } + } +} diff --git a/core/src/test/kotlin/net/corda/core/node/services/TimestampCheckerTests.kt b/core/src/test/kotlin/net/corda/core/node/services/TimestampCheckerTests.kt deleted file mode 100644 index 98462ae3d1..0000000000 --- a/core/src/test/kotlin/net/corda/core/node/services/TimestampCheckerTests.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.corda.core.node.services - -import net.corda.core.contracts.Timestamp -import net.corda.core.seconds -import org.junit.Test -import java.time.Clock -import java.time.Instant -import java.time.ZoneId -import kotlin.test.assertFalse -import kotlin.test.assertTrue - -class TimestampCheckerTests { - val clock = Clock.fixed(Instant.now(), ZoneId.systemDefault()) - val timestampChecker = TimestampChecker(clock, tolerance = 30.seconds) - - @Test - fun `should return true for valid timestamp`() { - val now = clock.instant() - val timestampPast = Timestamp(now - 60.seconds, now - 29.seconds) - val timestampFuture = Timestamp(now + 29.seconds, now + 60.seconds) - assertTrue { timestampChecker.isValid(timestampPast) } - assertTrue { timestampChecker.isValid(timestampFuture) } - } - - @Test - fun `should return false for invalid timestamp`() { - val now = clock.instant() - val timestampPast = Timestamp(now - 60.seconds, now - 31.seconds) - val timestampFuture = Timestamp(now + 31.seconds, now + 60.seconds) - assertFalse { timestampChecker.isValid(timestampPast) } - assertFalse { timestampChecker.isValid(timestampFuture) } - } -} diff --git a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt index dba3d63d17..297304d9d9 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/AttachmentSerializationTest.kt @@ -12,10 +12,12 @@ import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.unwrap import net.corda.flows.FetchAttachmentsFlow +import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.network.NetworkMapService import net.corda.node.services.persistence.NodeAttachmentService import net.corda.node.services.persistence.schemas.AttachmentEntity +import net.corda.node.services.statemachine.SessionInit import net.corda.node.utilities.transaction import net.corda.testing.node.MockNetwork import org.junit.After @@ -59,22 +61,22 @@ private fun NodeAttachmentService.updateAttachment(attachmentId: SecureHash, dat } class AttachmentSerializationTest { - private lateinit var network: MockNetwork + private lateinit var mockNet: MockNetwork private lateinit var server: MockNetwork.MockNode private lateinit var client: MockNetwork.MockNode @Before fun setUp() { - network = MockNetwork() - server = network.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - client = network.createNode(server.info.address) + mockNet = MockNetwork() + server = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + client = mockNet.createNode(server.info.address) client.disableDBCloseOnStop() // Otherwise the in-memory database may disappear (taking the checkpoint with it) while we reboot the client. - network.runNetwork() + mockNet.runNetwork() } @After fun tearDown() { - network.stopNodes() + mockNet.stopNodes() } private class ServerLogic(private val client: Party) : FlowLogic() { @@ -136,14 +138,18 @@ class AttachmentSerializationTest { } private fun launchFlow(clientLogic: ClientLogic, rounds: Int) { - server.services.registerServiceFlow(clientLogic.javaClass, ::ServerLogic) + server.internalRegisterFlowFactory(ClientLogic::class.java, object : InitiatedFlowFactory { + override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): ServerLogic { + return ServerLogic(otherParty) + } + }, ServerLogic::class.java, track = false) client.services.startFlow(clientLogic) - network.runNetwork(rounds) + mockNet.runNetwork(rounds) } private fun rebootClientAndGetAttachmentContent(checkAttachmentsOnLoad: Boolean = true): String { client.stop() - client = network.createNode(server.info.address, client.id, object : MockNetwork.Factory { + client = mockNet.createNode(server.info.address, client.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { return object : MockNetwork.MockNode(config, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun startMessagingService(rpcOps: RPCOps) { @@ -153,7 +159,7 @@ class AttachmentSerializationTest { } } }) - return (client.smm.allStateMachines[0].stateMachine.resultFuture.apply { network.runNetwork() }.getOrThrow() as ClientResult).attachmentContent + return (client.smm.allStateMachines[0].stateMachine.resultFuture.apply { mockNet.runNetwork() }.getOrThrow() as ClientResult).attachmentContent } @Test diff --git a/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt index 0ccad7ade3..46d4aa7499 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/CordaClassResolverTests.kt @@ -8,7 +8,12 @@ import net.corda.core.node.AttachmentClassLoaderTests import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.services.AttachmentStorage import net.corda.testing.node.MockAttachmentStorage +import org.junit.Rule import org.junit.Test +import org.junit.rules.ExpectedException +import java.lang.IllegalStateException +import java.sql.Connection +import java.util.* @CordaSerializable enum class Foo { @@ -160,4 +165,76 @@ class CordaClassResolverTests { CordaClassResolver(EmptyWhitelist).getRegistration(SubSubElement::class.java) CordaClassResolver(EmptyWhitelist).getRegistration(SerializableViaSuperSubInterface::class.java) } + + // Blacklist tests. + @get:Rule + val expectedEx = ExpectedException.none()!! + + @Test + fun `Check blacklisted class`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("Class java.util.HashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // HashSet is blacklisted. + resolver.getRegistration(HashSet::class.java) + } + + open class SubHashSet : HashSet() + @Test + fun `Check blacklisted subclass`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$SubHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubHashSet extends the blacklisted HashSet. + resolver.getRegistration(SubHashSet::class.java) + } + + class SubSubHashSet : SubHashSet() + @Test + fun `Check blacklisted subsubclass`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$SubSubHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubSubHashSet extends SubHashSet, which extends the blacklisted HashSet. + resolver.getRegistration(SubSubHashSet::class.java) + } + + class ConnectionImpl(val connection: Connection) : Connection by connection + @Test + fun `Check blacklisted interface impl`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.core.serialization.CordaClassResolverTests\$ConnectionImpl is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // ConnectionImpl implements blacklisted Connection. + resolver.getRegistration(ConnectionImpl::class.java) + } + + interface SubConnection : Connection + class SubConnectionImpl(val subConnection: SubConnection) : SubConnection by subConnection + @Test + fun `Check blacklisted super-interface impl`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superinterface java.sql.Connection of net.corda.core.serialization.CordaClassResolverTests\$SubConnectionImpl is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // SubConnectionImpl implements SubConnection, which extends the blacklisted Connection. + resolver.getRegistration(SubConnectionImpl::class.java) + } + + @Test + fun `Check forcibly allowed`() { + val resolver = CordaClassResolver(AllButBlacklisted) + // LinkedHashSet is allowed for serialization. + resolver.getRegistration(LinkedHashSet::class.java) + } + + @CordaSerializable + class CordaSerializableHashSet : HashSet() + @Test + fun `Check blacklist precedes CordaSerializable`() { + expectedEx.expect(IllegalStateException::class.java) + expectedEx.expectMessage("The superclass java.util.HashSet of net.corda.core.serialization.CordaClassResolverTests\$CordaSerializableHashSet is blacklisted, so it cannot be used in serialization.") + val resolver = CordaClassResolver(AllButBlacklisted) + // CordaSerializableHashSet is @CordaSerializable, but extends the blacklisted HashSet. + resolver.getRegistration(CordaSerializableHashSet::class.java) + } } \ No newline at end of file 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 521e0f2b20..0852846808 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/KryoTests.kt @@ -10,13 +10,13 @@ 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.bouncycastle.cert.X509CertificateHolder import org.junit.Before 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.security.cert.* import java.time.Instant import java.util.* import kotlin.test.assertEquals @@ -142,19 +142,20 @@ class KryoTests { } @Test - fun `serialize - deserialize X509Certififcate`() { - val expected = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) + fun `serialize - deserialize X509CertififcateHolder`() { + val expected: X509CertificateHolder = X509Utilities.createSelfSignedCACertificate(ALICE.name, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) val serialized = expected.serialize(kryo).bytes - val actual: X509Certificate = serialized.deserialize(kryo) + val actual: X509CertificateHolder = serialized.deserialize(kryo) assertEquals(expected, actual) } @Test fun `serialize - deserialize X509CertPath`() { + val certFactory = CertificateFactory.getInstance("X509") val rootCAKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCACert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootCAKey) val certificate = X509Utilities.createCertificate(CertificateType.TLS, rootCACert, rootCAKey, BOB.name, BOB_PUBKEY) - val expected = X509Utilities.createCertificatePath(rootCACert, certificate, revocationEnabled = false) + val expected = certFactory.generateCertPath(listOf(certificate.cert, rootCACert.cert)) val serialized = expected.serialize(kryo).bytes val actual: CertPath = serialized.deserialize(kryo) assertEquals(expected, actual) 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 e0e1cd17df..eed2941019 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/TransactionSerializationTests.kt @@ -107,11 +107,11 @@ class TransactionSerializationTests { } @Test - fun timestamp() { - tx.setTime(TEST_TX_TIME, 30.seconds) + fun timeWindow() { + tx.addTimeWindow(TEST_TX_TIME, 30.seconds) tx.signWith(MEGA_CORP_KEY) tx.signWith(DUMMY_NOTARY_KEY) val stx = tx.toSignedTransaction() - assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint) + assertEquals(TEST_TX_TIME, stx.tx.timeWindow?.midpoint) } } diff --git a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt index d76708db57..5896a3c292 100644 --- a/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt +++ b/core/src/test/kotlin/net/corda/core/serialization/amqp/SerializationOutputTests.kt @@ -1,10 +1,14 @@ package net.corda.core.serialization.amqp +import net.corda.core.flows.FlowException import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.EmptyWhitelist +import net.corda.nodeapi.RPCException +import net.corda.testing.MEGA_CORP_PUBKEY import org.apache.qpid.proton.codec.DecoderImpl import org.apache.qpid.proton.codec.EncoderImpl import org.junit.Test +import java.io.IOException import java.io.NotSerializableException import java.nio.ByteBuffer import java.util.* @@ -74,7 +78,14 @@ class SerializationOutputTests { override fun hashCode(): Int = ginger } - private fun serdes(obj: Any, factory: SerializerFactory = SerializerFactory()): Any { + @CordaSerializable + interface AnnotatedInterface + + data class InheritAnnotation(val foo: String) : AnnotatedInterface + + data class PolymorphicProperty(val foo: FooInterface?) + + private fun serdes(obj: Any, factory: SerializerFactory = SerializerFactory(), freshDeserializationFactory: SerializerFactory = SerializerFactory(), expectedEqual: Boolean = true): Any { val ser = SerializationOutput(factory) val bytes = ser.serialize(obj) @@ -93,15 +104,16 @@ class SerializationOutputTests { val result = decoder.readObject() as Envelope assertNotNull(result) - val des = DeserializationInput() + val des = DeserializationInput(freshDeserializationFactory) val desObj = des.deserialize(bytes) - assertTrue(Objects.deepEquals(obj, desObj)) + assertTrue(Objects.deepEquals(obj, desObj) == expectedEqual) // Now repeat with a re-used factory val ser2 = SerializationOutput(factory) val des2 = DeserializationInput(factory) val desObj2 = des2.deserialize(ser2.serialize(obj)) - assertTrue(Objects.deepEquals(obj, desObj2)) + assertTrue(Objects.deepEquals(obj, desObj2) == expectedEqual) + assertTrue(Objects.deepEquals(desObj, desObj2)) // TODO: add some schema assertions to check correctly formed. return desObj2 @@ -230,4 +242,109 @@ class SerializationOutputTests { val obj = MismatchType(456) serdes(obj) } + + @Test + fun `test custom serializers on public key`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.PublicKeySerializer()) + val obj = MEGA_CORP_PUBKEY + serdes(obj, factory, factory2) + } + + @Test + fun `test annotation is inherited`() { + val obj = InheritAnnotation("blah") + serdes(obj, SerializerFactory(EmptyWhitelist)) + } + + @Test + fun `test throwables serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) + + val obj = IllegalAccessException("message").fillInStackTrace() + serdes(obj, factory, factory2, false) + } + + @Test + fun `test complex throwables serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) + + try { + try { + throw IOException("Layer 1") + } catch(t: Throwable) { + throw IllegalStateException("Layer 2", t) + } + } catch(t: Throwable) { + serdes(t, factory, factory2, false) + } + } + + @Test + fun `test suppressed throwables serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) + + try { + try { + throw IOException("Layer 1") + } catch(t: Throwable) { + val e = IllegalStateException("Layer 2") + e.addSuppressed(t) + throw e + } + } catch(t: Throwable) { + serdes(t, factory, factory2, false) + } + } + + @Test + fun `test flow corda exception subclasses serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) + + val obj = FlowException("message").fillInStackTrace() + serdes(obj, factory, factory2) + } + + @Test + fun `test RPC corda exception subclasses serialize`() { + val factory = SerializerFactory() + factory.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory)) + + val factory2 = SerializerFactory() + factory2.register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(factory2)) + + val obj = RPCException("message").fillInStackTrace() + serdes(obj, factory, factory2) + } + + @Test + fun `test polymorphic property`() { + val obj = PolymorphicProperty(FooImplements("Ginger", 12)) + serdes(obj) + } + + @Test + fun `test null polymorphic property`() { + val obj = PolymorphicProperty(null) + serdes(obj) + } + } \ No newline at end of file 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 436ef4125e..bf08440e51 100644 --- a/core/src/test/kotlin/net/corda/core/testing/Generators.kt +++ b/core/src/test/kotlin/net/corda/core/testing/Generators.kt @@ -5,7 +5,8 @@ import com.pholser.junit.quickcheck.generator.Generator import com.pholser.junit.quickcheck.generator.java.util.ArrayListGenerator import com.pholser.junit.quickcheck.random.SourceOfRandomness import net.corda.core.contracts.* -import net.corda.core.crypto.* +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes @@ -118,9 +119,9 @@ class DurationGenerator : Generator(Duration::class.java) { } } -class TimestampGenerator : Generator(Timestamp::class.java) { - override fun generate(random: SourceOfRandomness, status: GenerationStatus): Timestamp { - return Timestamp(InstantGenerator().generate(random, status), DurationGenerator().generate(random, status)) +class TimeWindowGenerator : Generator(TimeWindow::class.java) { + override fun generate(random: SourceOfRandomness, status: GenerationStatus): TimeWindow { + return TimeWindow.withTolerance(InstantGenerator().generate(random, status), DurationGenerator().generate(random, status)) } } @@ -134,7 +135,7 @@ class X500NameGenerator : Generator(X500Name::class.java) { /** * Append something that looks a bit like a proper noun to the string builder. */ - private fun appendProperNoun(builder: StringBuilder, random: SourceOfRandomness, status: GenerationStatus) : StringBuilder { + private fun appendProperNoun(builder: StringBuilder, random: SourceOfRandomness) : StringBuilder { val length = random.nextByte(1, 8) val encoded = ByteBuffer.allocate(length.toInt()) encoded.put((random.nextByte(0, 25) + asciiA).toByte()) @@ -148,7 +149,7 @@ class X500NameGenerator : Generator(X500Name::class.java) { val wordCount = random.nextByte(1, 3) val cn = StringBuilder() for (word in 0..wordCount) { - appendProperNoun(cn, random, status).append(" ") + appendProperNoun(cn, random).append(" ") } return getTestX509Name(cn.trim().toString()) } diff --git a/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt new file mode 100644 index 0000000000..7490819a48 --- /dev/null +++ b/core/src/test/kotlin/net/corda/flows/TxKeyFlowTests.kt @@ -0,0 +1,51 @@ +package net.corda.flows + +import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty +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.node.MockNetwork +import org.junit.Before +import org.junit.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class TxKeyFlowTests { + lateinit var mockNet: MockNetwork + + @Before + fun before() { + mockNet = MockNetwork(false) + } + + @Test + fun `issue key`() { + // We run this in parallel threads to help catch any race conditions that may exist. + mockNet = MockNetwork(false, true) + + // Set up values we'll need + val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) + val alice: Party = aliceNode.services.myInfo.legalIdentity + val bob: Party = bobNode.services.myInfo.legalIdentity + aliceNode.services.identityService.registerIdentity(bobNode.info.legalIdentityAndCert) + aliceNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) + bobNode.services.identityService.registerIdentity(aliceNode.info.legalIdentityAndCert) + bobNode.services.identityService.registerIdentity(notaryNode.info.legalIdentityAndCert) + + // Run the flows + val requesterFlow = aliceNode.services.startFlow(TxKeyFlow.Requester(bob)) + + // Get the results + val actual: Map = requesterFlow.resultFuture.getOrThrow() + assertEquals(2, actual.size) + // Verify that the generated anonymous identities do not match the well known identities + val aliceAnonymousIdentity = actual[alice] ?: throw IllegalStateException() + val bobAnonymousIdentity = actual[bob] ?: throw IllegalStateException() + assertNotEquals(alice, aliceAnonymousIdentity.identity) + assertNotEquals(bob, bobAnonymousIdentity.identity) + } +} diff --git a/docs/source/_static/corda-cheat-sheet.pdf b/docs/source/_static/corda-cheat-sheet.pdf index 8ee358452a..c6378f1c27 100644 Binary files a/docs/source/_static/corda-cheat-sheet.pdf and b/docs/source/_static/corda-cheat-sheet.pdf differ diff --git a/docs/source/api-contracts.rst b/docs/source/api-contracts.rst new file mode 100644 index 0000000000..c011a098bd --- /dev/null +++ b/docs/source/api-contracts.rst @@ -0,0 +1,309 @@ +.. highlight:: kotlin +.. raw:: html + + + + +API: Contracts +============== + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-contracts`. + +All Corda contracts are JVM classes that implement ``net.corda.core.contracts.Contract``. + +The ``Contract`` interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + +Where: + +* ``verify(tx: TransactionForContract)`` determines whether transactions involving states which reference this + contract type are valid +* ``legalContractReference`` is the hash of the legal prose contract that ``verify`` seeks to express in code + +verify() +-------- + +``verify()`` is a method that doesn't return anything and takes a ``TransactionForContract`` as a parameter. It +either throws an exception if the transaction is considered invalid, or returns normally if the transaction is +considered valid. + +``verify()`` is executed in a sandbox. It does not have access to the enclosing scope, and is not able to access +the network or perform any other I/O. It only has access to the properties defined on ``TransactionForContract`` when +establishing whether a transaction is valid. + +The two simplest ``verify`` functions are the one that accepts all transactions, and the one that rejects all +transactions. + +Here is the ``verify`` that accepts all transactions: + +.. container:: codeset + + .. sourcecode:: kotlin + + override fun verify(tx: TransactionForContract) { + // Always accepts! + } + + .. sourcecode:: java + + @Override + public void verify(TransactionForContract tx) { + // Always accepts! + } + +And here is the ``verify`` that rejects all transactions: + +.. container:: codeset + + .. sourcecode:: kotlin + + override fun verify(tx: TransactionForContract) { + throw IllegalArgumentException("Always rejects!") + } + + .. sourcecode:: java + + @Override + public void verify(TransactionForContract tx) { + throw new IllegalArgumentException("Always rejects!"); + } + +TransactionForContract +^^^^^^^^^^^^^^^^^^^^^^ + +The ``TransactionForContract`` object passed into ``verify()`` represents the full set of information available to +``verify()`` when deciding whether to accept or reject the transaction. It has the following properties: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +Where: + +* ``inputs`` is a list of the transaction's inputs +* ``outputs`` is a list of the transaction's outputs +* ``attachments`` is a list of the transaction's attachments +* ``commands`` is a list of the transaction's commands, and their associated signatures +* ``origHash`` is the transaction's hash +* ``inputNotary`` is the transaction's notary +* ``timestamp`` is the transaction's timestamp + +requireThat() +^^^^^^^^^^^^^ + +Instead of throwing exceptions manually to reject a transaction, we can use the ``requireThat`` DSL: + +.. container:: codeset + + .. sourcecode:: kotlin + + requireThat { + "No inputs should be consumed when issuing an X." using (tx.inputs.isEmpty()) + "Only one output state should be created." using (tx.outputs.size == 1) + val out = tx.outputs.single() as XState + "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) + "All of the participants must be signers." using (command.signers.containsAll(out.participants)) + "The X's value must be non-negative." using (out.x.value > 0) + } + + .. sourcecode:: java + + requireThat(require -> { + require.using("No inputs should be consumed when issuing an X.", tx.getInputs().isEmpty()); + require.using("Only one output state should be created.", tx.getOutputs().size() == 1); + final XState out = (XState) tx.getOutputs().get(0); + require.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); + require.using("All of the participants must be signers.", command.getSigners().containsAll(out.getParticipants())); + require.using("The X's value must be non-negative.", out.getX().getValue() > 0); + return null; + }); + +For each <``String``, ``Boolean``> pair within ``requireThat``, if the boolean condition is false, an +``IllegalArgumentException`` is thrown with the corresponding string as the exception message. In turn, this +exception will cause the transaction to be rejected. + +Commands +^^^^^^^^ + +``TransactionForContract`` contains the commands as a list of ``AuthenticatedObject`` instances. +``AuthenticatedObject`` pairs an object with a list of signers. In this case, ``AuthenticatedObject`` pairs a command +with a list of the entities that are required to sign a transaction where this command is present: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 6 + :end-before: DOCEND 6 + +Where: + +* ``signers`` is the list of each signer's ``PublicKey`` +* ``signingParties`` is the list of the signer's identities, if known +* ``value`` is the object being signed (a command, in this case) + +Extracting commands +~~~~~~~~~~~~~~~~~~~ +You can use the ``requireSingleCommand()`` helper method to extract commands. + +`` Collection>.requireSingleCommand(klass: Class)`` asserts that +the transaction contains exactly one command of type ``T``, and returns it. If there is not exactly one command of this +type in the transaction, an exception is thrown, rejecting the transaction. + +For ``requireSingleCommand`` to work, all the commands that we wish to match against must be grouped using the same +marker interface. + +Here is an example of using ``requireSingleCommand`` to extract a transaction's command and using it to fork the +execution of ``verify()``: + +.. container:: codeset + + .. sourcecode:: kotlin + + class XContract : Contract { + interface Commands : CommandData { + class Issue : TypeOnlyCommandData(), Commands + class Transfer : TypeOnlyCommandData(), Commands + } + + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + when (command.value) { + is Commands.Issue -> { + // Issuance verification logic. + } + is Commands.Transfer -> { + // Transfer verification logic. + } + } + } + + override val legalContractReference: SecureHash = SecureHash.sha256("X contract hash") + } + + .. sourcecode:: java + + public class XContract implements Contract { + public interface Commands extends CommandData { + class Issue extends TypeOnlyCommandData implements Commands {} + class Transfer extends TypeOnlyCommandData implements Commands {} + } + + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Commands.class); + + if (command.getValue() instanceof Commands.Issue) { + // Issuance verification logic. + } else if (command.getValue() instanceof Commands.Transfer) { + // Transfer verification logic. + } + } + + private final SecureHash legalContractReference = SecureHash.sha256("X contract hash"); + @Override public final SecureHash getLegalContractReference() { return legalContractReference; } + } + +Grouping states +--------------- +Suppose we have the following transaction, where 15 USD is being exchanged for 10 GBP: + +.. image:: resources/ungrouped-tx.png + :scale: 20 + :align: center + +We can imagine that we would like to verify the USD states and the GBP states separately: + +.. image:: resources/grouped-tx.png + :scale: 20 + :align: center + +``TransactionForContract`` provides a ``groupStates`` method to allow you to group states in this way: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + +Where ``InOutGroup`` is defined as: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/TransactionVerification.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + +For example, we could group the states in the transaction above by currency (i.e. by ``amount.token``): + +.. container:: codeset + + .. sourcecode:: kotlin + + val groups: List>> = tx.groupStates(Cash.State::class.java) { + it -> it.amount.token + } + + .. sourcecode:: java + + final List>> groups = tx.groupStates( + Cash.State.class, + it -> it.getAmount().getToken() + ); + +This would produce the following InOutGroups: + +.. image:: resources/in-out-groups.png + +We can now verify these groups individually: + +.. container:: codeset + + .. sourcecode:: kotlin + + for ((in_, out, key) in groups) { + when (key) { + is GBP -> { + // GBP verification logic. + } + is USD -> { + // USD verification logic. + } + } + } + + .. sourcecode:: java + + for (InOutGroup group : groups) { + if (group.getGroupingKey() == USD) { + // USD verification logic. + } else if (group.getGroupingKey() == GBP) { + // GBP verification logic. + } + } + +Legal prose +----------- + +Current, ``legalContractReference`` is simply the SHA-256 hash of a contract: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + +In the future, a contract's legal prose will be included as an attachment instead. \ No newline at end of file diff --git a/docs/source/api-core-types.rst b/docs/source/api-core-types.rst new file mode 100644 index 0000000000..2505068675 --- /dev/null +++ b/docs/source/api-core-types.rst @@ -0,0 +1,94 @@ +API: Core types +=============== + +Corda provides a large standard library of data types used to represent the Corda data model. In addition, there are a +series of helper libraries which provide date manipulation, maths and cryptography functions. + +Cryptography and maths support +------------------------------ +The ``SecureHash`` class represents a secure hash of unknown algorithm. We currently define only a single subclass, +``SecureHash.SHA256``. There are utility methods to create them, parse them and so on. + +We also provide some mathematical utilities, in particular a set of interpolators and classes for working with +splines. These can be found in the `maths package `_. + +NamedByHash and UniqueIdentifier +-------------------------------- +Things which are identified by their hash, like transactions and attachments, should implement the ``NamedByHash`` +interface which standardises how the ID is extracted. Note that a hash is *not* a globally unique identifier: it +is always a derivative summary of the contents of the underlying data. Sometimes this isn't what you want: +two deals that have exactly the same parameters and which are made simultaneously but which are logically different +can't be identified by hash because their contents would be identical. + +Instead you would use ``UniqueIdentifier``. This is a combination of a (Java) ``UUID`` representing a globally +unique 128 bit random number, and an arbitrary string which can be paired with it. For instance the string may +represent an existing "weak" (not guaranteed unique) identifier for convenience purposes. + +Party and CompositeKey +---------------------- +Entities using the network are called *parties*. Parties can sign structures using keys, and a party may have many +keys under their control. + +Parties can be represented either in full (including name) or pseudonymously, using the ``Party`` or ``AnonymousParty`` +classes respectively. For example, in a transaction sent to your node as part of a chain of custody it is important you +can convince yourself of the transaction's validity, but equally important that you don't learn anything about who was +involved in that transaction. In these cases ``AnonymousParty`` should be used, which contains a public key (may be a composite key) +without any identifying information about who owns it. In contrast, for internal processing where extended details of +a party are required, the ``Party`` class should be used. The identity service provides functionality for resolving +anonymous parties to full parties. + +.. note:: These types are provisional and will change significantly in future as the identity framework becomes more +fleshed out. + +AuthenticatedObject +------------------- +An ``AuthenticatedObject`` represents an object (like a command) and the list of associated signers. + +Multi-signature support +----------------------- +Corda supports scenarios where more than one key or party is required to authorise a state object transition, for example: +"Either the CEO or 3 out of 5 of his assistants need to provide signatures". + +.. _composite-keys: + +Composite Keys +^^^^^^^^^^^^^^ +This is achieved by public key composition, using a tree data structure ``CompositeKey``. A ``CompositeKey`` is a tree that +stores the cryptographic public key primitives in its leaves and the composition logic in the intermediary nodes. Every intermediary +node specifies a *threshold* of how many child signatures it requires. + +An illustration of an *"either Alice and Bob, or Charlie"* composite key: + +.. image:: resources/composite-key.png + :align: center + :width: 300px + +To allow further flexibility, each child node can have an associated custom *weight* (the default is 1). The *threshold* +then specifies the minimum total weight of all children required. Our previous example can also be expressed as: + +.. image:: resources/composite-key-2.png + :align: center + :width: 300px + +Verification +^^^^^^^^^^^^ +Signature verification is performed in two stages: + + 1. Given a list of signatures, each signature is verified against the expected content. + 2. The public keys corresponding to the signatures are matched against the leaves of the composite key tree in question, + and the total combined weight of all children is calculated for every intermediary node. If all thresholds are satisfied, + the composite key requirement is considered to be met. + +Date support +------------ +There are a number of supporting interfaces and classes for use by contracts which deal with dates (especially in the +context of deadlines). As contract negotiation typically deals with deadlines in terms such as "overnight", "T+3", +etc., it's desirable to allow conversion of these terms to their equivalent deadline. ``Tenor`` models the interval +before a deadline, such as 3 days, etc., while ``DateRollConvention`` describes how deadlines are modified to take +into account bank holidays or other events that modify normal working days. + +Calculating the rollover of a deadline based on working days requires information on the bank holidays involved +(and where a contract's parties are in different countries, for example, this can involve multiple separate sets of +bank holidays). The ``BusinessCalendar`` class models these calendars of business holidays; currently it loads these +from files on disk, but in future this is likely to involve reference data oracles in order to ensure consensus on the +dates used. diff --git a/docs/source/api-flows.rst b/docs/source/api-flows.rst new file mode 100644 index 0000000000..7c57be9503 --- /dev/null +++ b/docs/source/api-flows.rst @@ -0,0 +1,300 @@ +.. highlight:: kotlin +.. raw:: html + + + + +API: Flows +========== + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-flows`. + +An example flow +--------------- +Let's imagine a flow for agreeing a basic ledger update between Alice and Bob. This flow will have two sides: + +* An ``Initiator`` side, that will initiate the request to update the ledger +* A ``Responder`` side, that will respond to the request to update the ledger + +Initiator +^^^^^^^^^ +In our flow, the Initiator flow class will be doing the majority of the work: + +*Part 1 - Build the transaction* + +1. Choose a notary for the transaction +2. Create a transaction builder +3. Extract any input states from the vault and add them to the builder +4. Create any output states and add them to the builder +5. Add any commands, attachments and timestamps to the builder + +*Part 2 - Sign the transaction* + +6. Sign the transaction builder +7. Convert the builder to a signed transaction + +*Part 3 - Verify the transaction* + +8. Verify the transaction by running its contracts + +*Part 4 - Gather the counterparty's signature* + +9. Send the transaction to the counterparty +10. Wait to receive back the counterparty's signature +11. Add the counterparty's signature to the transaction +12. Verify the transaction's signatures + +*Part 5 - Finalize the transaction* + +13. Send the transaction to the notary +14. Wait to receive back the notarised transaction +15. Record the transaction locally +16. Store any relevant states in the vault +17. Send the transaction to the counterparty for recording + +We can visualize the work performed by initiator as follows: + +.. image:: resources/flow-overview.png + +Responder +^^^^^^^^^ +To respond to these actions, the responder takes the following steps: + +*Part 1 - Sign the transaction* + +1. Receive the transaction from the counterparty +2. Verify the transaction's existing signatures +3. Verify the transaction by running its contracts +4. Generate a signature over the transaction +5. Send the signature back to the counterparty + +*Part 2 - Record the transaction* + +6. Receive the notarised transaction from the counterparty +7. Record the transaction locally +8. Store any relevant states in the vault + +FlowLogic +--------- +In practice, a flow is implemented as one or more communicating ``FlowLogic`` subclasses. Each ``FlowLogic`` subclass +must override ``FlowLogic.call()``, which describes the actions it will take as part of the flow. + +So in the example above, we would have an ``Initiator`` ``FlowLogic`` subclass and a ``Responder`` ``FlowLogic`` +subclass. The actions of the initiator's side of the flow would be defined in ``Initiator.call``, and the actions +of the responder's side of the flow would be defined in ``Responder.call``. + +FlowLogic annotations +^^^^^^^^^^^^^^^^^^^^^ +Any flow that you wish to start either directly via RPC or as a subflow must be annotated with the +``@InitiatingFlow`` annotation. Additionally, if you wish to start the flow via RPC, you must annotate it with the +``@StartableByRPC`` annotation. + +Any flow that responds to a message from another flow must be annotated with the ``@InitiatedBy`` annotation. +``@InitiatedBy`` takes the class of the flow it is responding to as its single parameter. + +So in our example, we would have: + +.. container:: codeset + + .. sourcecode:: kotlin + + @InitiatingFlow + @StartableByRPC + class Initiator(): FlowLogic() { + + ... + + @InitiatedBy(Initiator::class) + class Responder(val otherParty: Party) : FlowLogic() { + + .. sourcecode:: java + + @InitiatingFlow + @StartableByRPC + public static class Initiator extends FlowLogic { + + ... + + @InitiatedBy(Initiator.class) + public static class Responder extends FlowLogic { + +Additionally, any flow that is started by a ``SchedulableState`` must be annotated with the ``@SchedulableFlow`` +annotation. + +ServiceHub +---------- +Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the +various services the node provides. See :doc:`api-service-hub` for information about the services the ``ServiceHub`` +offers. + +Some common tasks performed using the ``ServiceHub`` are: + +* Looking up your own identity or the identity of a counterparty using the ``networkMapCache`` +* Identifying the providers of a given service (e.g. a notary service) using the ``networkMapCache`` +* Retrieving states to use in a transaction using the ``vaultService`` +* Retrieving attachments and past transactions to use in a transaction using the ``storageService`` +* Creating a timestamp using the ``clock`` +* Signing a transaction using the ``keyManagementService`` + +Common flow tasks +----------------- +There are a number of common tasks that you will need to perform within ``FlowLogic.call`` in order to agree ledger +updates. This section details the API for the most common tasks. + +Retrieving information about other nodes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We use the network map to retrieve information about other nodes on the network: + +.. container:: codeset + + .. sourcecode:: kotlin + + val networkMap = serviceHub.networkMapCache + + val allNodes = networkMap.partyNodes + val allNotaryNodes = networkMap.notaryNodes + val randomNotaryNode = networkMap.getAnyNotary() + + val alice = networkMap.getNodeByLegalName(X500Name("CN=Alice,O=Alice,L=London,C=GB")) + val bob = networkMap.getNodeByLegalIdentityKey(bobsKey) + + .. sourcecode:: java + + final NetworkMapCache networkMap = getServiceHub().getNetworkMapCache(); + + final List allNodes = networkMap.getPartyNodes(); + final List allNotaryNodes = networkMap.getNotaryNodes(); + final Party randomNotaryNode = networkMap.getAnyNotary(null); + + final NodeInfo alice = networkMap.getNodeByLegalName(new X500Name("CN=Alice,O=Alice,L=London,C=GB")); + final NodeInfo bob = networkMap.getNodeByLegalIdentityKey(bobsKey); + +Communication between parties +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +``FlowLogic`` instances communicate using three functions: + +* ``send(otherParty: Party, payload: Any)`` + * Sends the ``payload`` object to the ``otherParty`` +* ``receive(receiveType: Class, otherParty: Party)`` + * Receives an object of type ``receiveType`` from the ``otherParty`` +* ``sendAndReceive(receiveType: Class, otherParty: Party, payload: Any)`` + * Sends the ``payload`` object to the ``otherParty``, and receives an object of type ``receiveType`` back + +Each ``FlowLogic`` subclass can be annotated to respond to messages from a given *counterparty* flow using the +``@InitiatedBy`` annotation. When a node first receives a message from a given ``FlowLogic.call()`` invocation, it +responds as follows: + +* The node checks whether they have a ``FlowLogic`` subclass that is registered to respond to the ``FlowLogic`` that + is sending the message: + + a. If yes, the node starts an instance of this ``FlowLogic`` by invoking ``FlowLogic.call()`` + b. Otherwise, the node ignores the message + +* The counterparty steps through their ``FlowLogic.call()`` method until they encounter a call to ``receive()``, at + which point they process the message from the initiator + +Upon calling ``receive()``/``sendAndReceive()``, the ``FlowLogic`` is suspended until it receives a response. + +UntrustworthyData +~~~~~~~~~~~~~~~~~ +``send()`` and ``sendAndReceive()`` return a payload wrapped in an ``UntrustworthyData`` instance. This is a +reminder that any data received off the wire is untrustworthy and must be verified. + +We verify the ``UntrustworthyData`` and retrieve its payload by calling ``unwrap``: + +.. container:: codeset + + .. sourcecode:: kotlin + + val partSignedTx = receive(otherParty).unwrap { partSignedTx -> + val wireTx = partSignedTx.verifySignatures(keyPair.public, notaryPubKey) + wireTx.toLedgerTransaction(serviceHub).verify() + partSignedTx + } + + .. sourcecode:: java + + final SignedTransaction partSignedTx = receive(SignedTransaction.class, otherParty) + .unwrap(tx -> { + try { + final WireTransaction wireTx = tx.verifySignatures(keyPair.getPublic(), notaryPubKey); + wireTx.toLedgerTransaction(getServiceHub()).verify(); + } catch (SignatureException ex) { + throw new FlowException(tx.getId() + " failed signature checks", ex); + } + return tx; + }); + +Subflows +-------- +Corda provides a number of built-in flows that should be used for handling common tasks. The most important are: + +* ``CollectSignaturesFlow``, which should be used to collect a transaction's required signatures +* ``FinalityFlow``, which should be used to notarise and record a transaction +* ``ResolveTransactionsFlow``, which should be used to verify the chain of inputs to a transaction +* ``ContractUpgradeFlow``, which should be used to change a state's contract +* ``NotaryChangeFlow``, which should be used to change a state's notary + +These flows are designed to be used as building blocks in your own flows. You invoke them by calling +``FlowLogic.subFlow`` from within your flow's ``call`` method. Here is an example from ``TwoPartyDealFlow.kt``: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 12 + +In this example, we are starting a ``CollectSignaturesFlow``, passing in a partially signed transaction, and +receiving back a fully-signed version of the same transaction. + +Subflows in our example flow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +In practice, many of the actions in our example flow would be automated using subflows: + +* Parts 2-4 of ``Initiator.call`` should be automated by invoking ``CollectSignaturesFlow`` +* Part 5 of ``Initiator.call`` should be automated by invoking ``FinalityFlow`` +* Part 1 of ``Responder.call`` should be automated by invoking ``SignTransactionFlow`` +* Part 2 of ``Responder.call`` will be handled automatically when the counterparty invokes ``FinalityFlow`` + +FlowException +------------- +Suppose a node throws an exception while running a flow. Any counterparty flows waiting for a message from the node +(i.e. as part of a call to ``receive`` or ``sendAndReceive``) will be notified that the flow has unexpectedly +ended and will themselves end. However, the exception thrown will not be propagated back to the counterparties. + +If you wish to notify any waiting counterparties of the cause of the exception, you can do so by throwing a +``FlowException``: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/flows/FlowException.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +The flow framework will automatically propagate the ``FlowException`` back to the waiting counterparties. + +There are many scenarios in which throwing a ``FlowException`` would be appropriate: + +* A transaction doesn't ``verify()`` +* A transaction's signatures are invalid +* The transaction does not match the parameters of the deal as discussed +* You are reneging on a deal + +Suspending flows +---------------- +In order for nodes to be able to run multiple flows concurrently, and to allow flows to survive node upgrades and +restarts, flows need to be checkpointable and serializable to disk. + +This is achieved by marking any function invoked from within ``FlowLogic.call()`` with an ``@Suspendable`` annotation. + +We can see an example in ``CollectSignaturesFlow``: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 \ No newline at end of file diff --git a/docs/source/api-index.rst b/docs/source/api-index.rst new file mode 100644 index 0000000000..b50e7be53a --- /dev/null +++ b/docs/source/api-index.rst @@ -0,0 +1,13 @@ +API +=== + +This section describes the APIs that are available for the development of CorDapps: + +* :doc:`api-states` +* :doc:`api-persistence` +* :doc:`api-contracts` +* :doc:`api-transactions` +* :doc:`api-flows` +* :doc:`api-core-types` + +Before reading this page, you should be familiar with the key concepts of Corda: :doc:`key-concepts`. \ No newline at end of file diff --git a/docs/source/persistence.rst b/docs/source/api-persistence.rst similarity index 51% rename from docs/source/persistence.rst rename to docs/source/api-persistence.rst index 0abf7fcb31..120063b73f 100644 --- a/docs/source/persistence.rst +++ b/docs/source/api-persistence.rst @@ -1,23 +1,28 @@ -Persistence -=========== +.. highlight:: kotlin +.. raw:: html -Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping* (ORM) tool -to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing + + + +API: Persistence +================ + +Corda offers developers the option to expose all or some part of a contract state to an *Object Relational Mapping* +(ORM) tool to be persisted in a RDBMS. The purpose of this is to assist *vault* development by effectively indexing persisted contract states held in the vault for the purpose of running queries over them and to allow relational joins between Corda data and private data local to the organisation owning a node. -The ORM mapping is specified using the `Java Persistence API `_ (JPA) -as annotations and is converted to database table rows by the node automatically every time a state is recorded in the -node's local vault as part of a transaction. +The ORM mapping is specified using the `Java Persistence API `_ +(JPA) as annotations and is converted to database table rows by the node automatically every time a state is recorded +in the node's local vault as part of a transaction. -.. note:: Presently the node includes an instance of the H2 database but any database that supports JDBC is a candidate and - the node will in the future support a range of database implementations via their JDBC drivers. Much of the node - internal state is also persisted there. You can access the internal H2 database via JDBC, please see the info - in ":doc:`node-administration`" for details. +.. note:: Presently the node includes an instance of the H2 database but any database that supports JDBC is a + candidate and the node will in the future support a range of database implementations via their JDBC drivers. Much + of the node internal state is also persisted there. You can access the internal H2 database via JDBC, please see the + info in ":doc:`node-administration`" for details. Schemas ------- - Every ``ContractState`` can implement the ``QueryableState`` interface if it wishes to be inserted into the node's local database and accessible using SQL. @@ -26,10 +31,10 @@ database and accessible using SQL. :start-after: DOCSTART QueryableState :end-before: DOCEND QueryableState -The ``QueryableState`` interface requires the state to enumerate the different relational schemas it supports, for instance in -cases where the schema has evolved, with each one being represented by a ``MappedSchema`` object return by the -``supportedSchemas()`` method. Once a schema is selected it must generate that representation when requested via the -``generateMappedObject()`` method which is then passed to the ORM. +The ``QueryableState`` interface requires the state to enumerate the different relational schemas it supports, for +instance in cases where the schema has evolved, with each one being represented by a ``MappedSchema`` object return +by the ``supportedSchemas()`` method. Once a schema is selected it must generate that representation when requested +via the ``generateMappedObject()`` method which is then passed to the ORM. Nodes have an internal ``SchemaService`` which decides what to persist and what not by selecting the ``MappedSchema`` to use. @@ -46,44 +51,44 @@ to use. The ``SchemaService`` can be configured by a node administrator to select the schemas used by each app. In this way the relational view of ledger states can evolve in a controlled fashion in lock-step with internal systems or other -integration points and not necessarily with every upgrade to the contract code. -It can select from the ``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a -later version of a schema or even provide a ``MappedSchema`` not originally offered by the ``QueryableState``. +integration points and not necessarily with every upgrade to the contract code. It can select from the +``MappedSchema`` offered by a ``QueryableState``, automatically upgrade to a later version of a schema or even +provide a ``MappedSchema`` not originally offered by the ``QueryableState``. It is expected that multiple different contract state implementations might provide mappings to some common schema. For example an Interest Rate Swap contract and an Equity OTC Option contract might both provide a mapping to a common Derivative schema. The schemas should typically not be part of the contract itself and should exist independently of it to encourage re-use of a common set within a particular business area or Cordapp. -``MappedSchema`` offer a family name that is disambiguated using Java package style name-spacing derived from the class name -of a *schema family* class that is constant across versions, allowing the ``SchemaService`` to select a preferred version -of a schema. +``MappedSchema`` offer a family name that is disambiguated using Java package style name-spacing derived from the +class name of a *schema family* class that is constant across versions, allowing the ``SchemaService`` to select a +preferred version of a schema. -The ``SchemaService`` is also responsible for the ``SchemaOptions`` that can be configured for a particular ``MappedSchema`` -which allow the configuration of a database schema or table name prefixes to avoid any clash with other ``MappedSchema``. +The ``SchemaService`` is also responsible for the ``SchemaOptions`` that can be configured for a particular +``MappedSchema`` which allow the configuration of a database schema or table name prefixes to avoid any clash with +other ``MappedSchema``. -.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer the version upgrading and - additional schemas as part of Cordapps, and that the active schemas be configurable. However the present implementation - offers none of this and simply results in all versions of all schemas supported by a ``QueryableState`` being persisted. - This will change in due course. Similarly, it does not currently support configuring ``SchemaOptions`` but will do so in - the future. +.. note:: It is intended that there should be plugin support for the ``SchemaService`` to offer the version upgrading + and additional schemas as part of Cordapps, and that the active schemas be configurable. However the present + implementation offers none of this and simply results in all versions of all schemas supported by a + ``QueryableState`` being persisted. This will change in due course. Similarly, it does not currently support + configuring ``SchemaOptions`` but will do so in the future. Object relational mapping ------------------------- - -The persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass, constructed -either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always associate a -``StateRef`` with a persisted representation of a ``ContractState`` and allows joining with the set of unconsumed states -in the vault. +The persisted representation of a ``QueryableState`` should be an instance of a ``PersistentState`` subclass, +constructed either by the state itself or a plugin to the ``SchemaService``. This allows the ORM layer to always +associate a ``StateRef`` with a persisted representation of a ``ContractState`` and allows joining with the set of +unconsumed states in the vault. The ``PersistentState`` subclass should be marked up as a JPA 2.1 *Entity* with a defined table name and having properties (in Kotlin, getters/setters in Java) annotated to map to the appropriate columns and SQL types. Additional entities can be included to model these properties where they are more complex, for example collections, so the mapping -does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema in order -to initialise the ORM layer. +does not have to be *flat*. The ``MappedSchema`` must provide a list of all of the JPA entity classes for that schema +in order to initialise the ORM layer. Several examples of entities and mappings are provided in the codebase, including ``Cash.State`` and -``CommercialPaper.State``. For example, here's the first version of the cash schema. +``CommercialPaper.State``. For example, here's the first version of the cash schema. .. literalinclude:: ../../finance/src/main/kotlin/net/corda/schemas/CashSchemaV1.kt :language: kotlin diff --git a/docs/source/api-rpc.rst b/docs/source/api-rpc.rst new file mode 100644 index 0000000000..8f37fa9beb --- /dev/null +++ b/docs/source/api-rpc.rst @@ -0,0 +1,29 @@ +API: RPC operations +=================== +The node's owner interacts with the node solely via remote procedure calls (RPC). The node's owner does not have +access to the node's ``ServiceHub``. + +The key RPC operations exposed by the node are: + +* ``CordaRPCOps.vaultQueryBy`` + * Extract states from the node's vault based on a query criteria +* ``CordaRPCOps.vaultTrackBy`` + * As above, but also returns an observable of future states matching the query +* ``CordaRPCOps.verifiedTransactions`` + * Extract all transactions from the node's local storage, as well as an observable of all future transactions +* ``CordaRPCOps.networkMapUpdates`` + * A list of network nodes, and an observable of changes to the network map +* ``CordaRPCOps.registeredFlows`` + * See a list of registered flows on the node +* ``CordaRPCOps.startFlowDynamic`` + * Start one of the node's registered flows +* ``CordaRPCOps.startTrackedFlowDynamic`` + * As above, but also returns a progress handle for the flow +* ``CordaRPCOps.nodeIdentity`` + * Returns the node's identity +* ``CordaRPCOps.currentNodeTime`` + * Returns the node's current time +* ``CordaRPCOps.partyFromKey/CordaRPCOps.partyFromX500Name`` + * Retrieves a party on the network based on a public key or X500 name +* ``CordaRPCOps.uploadAttachment``/``CordaRPCOps.openAttachment``/``CordaRPCOps.attachmentExists`` + * Uploads, opens and checks for the existence of attachments \ No newline at end of file diff --git a/docs/source/api-service-hub.rst b/docs/source/api-service-hub.rst new file mode 100644 index 0000000000..739dee94bd --- /dev/null +++ b/docs/source/api-service-hub.rst @@ -0,0 +1,29 @@ +API: ServiceHub +=============== +Within ``FlowLogic.call``, the flow developer has access to the node's ``ServiceHub``, which provides access to the +various services the node provides. The services offered by the ``ServiceHub`` are split into the following categories: + +* ``ServiceHub.networkMapCache`` + * Provides information on other nodes on the network (e.g. notaries…) +* ``ServiceHub.identityService`` + * Allows you to resolve anonymous identities to well-known identities if you have the required certificates +* ``ServiceHub.vaultService`` + * Stores the node’s current and historic states +* ``ServiceHub.storageService`` + * Stores additional information such as transactions and attachments +* ``ServiceHub.keyManagementService`` + * Manages signing transactions and generating fresh public keys +* ``ServiceHub.myInfo`` + * Other information about the node +* ``ServiceHub.clock`` + * Provides access to the node’s internal time and date + +Additional, ``ServiceHub`` exposes the following properties: + +* ``ServiceHub.loadState`` and ``ServiceHub.toStateAndRef`` to resolve a ``StateRef`` into a ``TransactionState`` or + a ``StateAndRef`` +* ``ServiceHub.signInitialTransaction`` to sign a ``TransactionBuilder`` and convert it into a ``SignedTransaction`` +* ``ServiceHub.createSignature`` and ``ServiceHub.addSignature`` to create and add signatures to a ``SignedTransaction`` + +Finally, ``ServiceHub`` exposes the node's legal identity key (via ``ServiceHub.legalIdentityKey``) and its notary +identity key (via ``ServiceHub.notaryIdentityKey``). \ No newline at end of file diff --git a/docs/source/api-states.rst b/docs/source/api-states.rst new file mode 100644 index 0000000000..c1c0befbe7 --- /dev/null +++ b/docs/source/api-states.rst @@ -0,0 +1,153 @@ +API: States +=========== + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-states`. + +ContractState +------------- +In Corda, states are classes that implement ``ContractState``. The ``ContractState`` interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +Where: + +* ``contract`` is the ``Contract`` class defining the constraints on transactions involving states of this type +* ``participants`` is a ``List`` of the ``AbstractParty`` who are considered to have a stake in the state. For example, + all the ``participants`` will: + + * Need to sign a notary-change transaction for this state + * Receive any committed transactions involving this state as part of ``FinalityFlow`` + +The vault +--------- +Each node has a vault, where it stores the states that are "relevant" to the node's owner. Whenever the node sees a +new transaction, it performs a relevancy check to decide whether to add each of the transaction's output states to +its vault. The default vault implementation decides whether a state is relevant as follows: + + * The vault will store any state for which it is one of the ``participants`` + * This behavior is overridden for states that implement ``LinearState`` or ``OwnableState`` (see below) + +If a state is not considered relevant, the node will still store the transaction in its local storage, but it will +not track the transaction's states in its vault. + +ContractState sub-interfaces +---------------------------- +There are two common optional sub-interfaces of ``ContractState``: + +* ``LinearState``, which helps represent objects that have a constant identity over time +* ``OwnableState``, which helps represent fungible assets + +For example, a cash is an ``OwnableState`` - you don't have a specific piece of cash you are tracking over time, but +rather a total amount of cash that you can combine and divide at will. A contract, on the other hand, cannot be +merged with other contracts of the same type - it has a unique separate identity over time. + +We can picture the hierarchy as follows: + +.. image:: resources/state-hierarchy.png + +LinearState +^^^^^^^^^^^ +``LinearState`` models facts that have a constant identity over time. Remember that in Corda, states are immutable and +can't be updated directly. Instead, we represent an evolving fact as a sequence of states where every state is a +``LinearState`` that shares the same ``linearId``. Each sequence of linear states represents the lifecycle of a given +fact up to the current point in time. It represents the historic audit trail of how the fact evolved over time to its +current "state". + +The ``LinearState`` interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + +Where: + +* ``linearId`` is a ``UniqueIdentifier`` that: + + * Allows the successive versions of the fact to be linked over time + * Provides an ``externalId`` for referencing the state in external systems + +* ``isRelevant(ourKeys: Set)`` overrides the default vault implementation's relevancy check. You would + generally override it to check whether ``ourKeys`` is relevant to the state at hand in some way. + +The vault tracks the head (i.e. the most recent version) of each ``LinearState`` chain (i.e. each sequence of +states all sharing a ``linearId``). To create a transaction updating a ``LinearState``, we retrieve the state from the +vault using its ``linearId``. + +UniqueIdentifier +~~~~~~~~~~~~~~~~ +``UniqueIdentifier`` is a combination of a (Java) ``UUID`` representing a globally unique 128 bit random number, and +an arbitrary string which can be paired with it. For instance the string may represent an existing "weak" (not +guaranteed unique) identifier for convenience purposes. + +OwnableState +^^^^^^^^^^^^ +``OwnableState`` models fungible assets. Fungible assets are assets for which it's the total amount held that is +important, rather than the actual units held. US dollars are an example of a fungible asset - we do not track the +individual dollar bills held, but rather the total amount of dollars. + +The ``OwnableState`` interface is defined as follows: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + +Where: + +* ``owner`` is the ``PublicKey`` of the asset's owner + + * ``OwnableState`` also override the default behavior of the vault's relevancy check. The default vault + implementation will track any ``OwnableState`` of which it is the owner. + +* ``withNewOwner(newOwner: PublicKey)`` creates an identical copy of the state, only with a new owner + +Other interfaces +^^^^^^^^^^^^^^^^ +``ContractState`` has several more sub-interfaces that can optionally be implemented: + +* ``QueryableState``, which allows the state to be queried in the node's database using SQL (see + :doc:`api-persistence`) +* ``SchedulableState``, which allows us to schedule future actions for the state (e.g. a coupon on a bond) (see + :doc:`event-scheduling`) + +User-defined fields +------------------- +Beyond implementing ``LinearState`` or ``OwnableState``, the definition of the state is up to the CorDapp developer. +You can define any additional class fields and methods you see fit. + +For example, here is a relatively complex state definition, for a state representing cash: + +.. container:: codeset + + .. literalinclude:: ../../finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +TransactionState +---------------- +When a ``ContractState`` is added to a ``TransactionBuilder``, it is wrapped in a ``TransactionState``: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 4 + :end-before: DOCEND 4 + +Where: + +* ``data`` is the state to be stored on-ledger +* ``notary`` is the notary service for this state +* ``encumbrance`` points to another state that must also appear as an input to any transaction consuming this + state \ No newline at end of file diff --git a/docs/source/api-transactions.rst b/docs/source/api-transactions.rst new file mode 100644 index 0000000000..9b736b3ce8 --- /dev/null +++ b/docs/source/api-transactions.rst @@ -0,0 +1,299 @@ +.. highlight:: kotlin +.. raw:: html + + + + +API: Transactions +================= + +.. note:: Before reading this page, you should be familiar with the key concepts of :doc:`key-concepts-transactions`. + +Transaction types +----------------- +There are two types of transaction in Corda: + +* ``TransactionType.NotaryChange``, used to change the notary for a set of states +* ``TransactionType.General``, for transactions other than notary-change transactions + +Notary-change transactions +^^^^^^^^^^^^^^^^^^^^^^^^^^ +A single Corda network will usually have multiple notary services. To commit a transaction, we require a signature +from the notary service associated with each input state. If we tried to commit a transaction where the input +states were associated with different notary services, the transaction would require a signature from multiple notary +services, creating a complicated multi-phase commit scenario. To prevent this, every input state in a transaction +must be associated the same notary. + +However, we will often need to create a transaction involving input states associated with different notaries. Before +we can create this transaction, we will need to change the notary service associated with each state by: + +* Deciding which notary service we want to notarise the transaction +* For each set of inputs states that point to the same notary service that isn't the desired notary service, creating a + ``TransactionType.NotaryChange`` transaction that: + + * Consumes the input states pointing to the old notary + * Outputs the same states, but that now point to the new notary + +* Using the outputs of the notary-change transactions as inputs to a standard ``TransactionType.General`` transaction + +In practice, this process is handled automatically by a built-in flow called ``NotaryChangeFlow``. See +:doc:`api-flows` for more details. + +Transaction workflow +-------------------- +There are four states the transaction can occupy: + +* ``TransactionBuilder``, a mutable transaction-in-construction +* ``WireTransaction``, an immutable transaction +* ``SignedTransaction``, a ``WireTransaction`` with 1+ associated signatures +* ``LedgerTransaction``, a resolved ``WireTransaction`` that can be checked for contract validity + +Here are the possible transitions between transaction states: + +.. image:: resources/transaction-flow.png + +TransactionBuilder +------------------ +Creating a builder +^^^^^^^^^^^^^^^^^^ +The first step when building a transaction is to create a ``TransactionBuilder``: + +.. container:: codeset + + .. sourcecode:: kotlin + + // A general transaction builder. + val generalTxBuilder = TransactionType.General.Builder() + + // A notary-change transaction builder. + val notaryChangeTxBuilder = TransactionType.NotaryChange.Builder() + + .. sourcecode:: java + + // A general transaction builder. + final TransactionBuilder generalTxBuilder = new TransactionType.General.Builder(); + + // A notary-change transaction builder. + final TransactionBuilder notaryChangeTxBuilder = new TransactionType.NotaryChange.Builder(); + +Adding items +^^^^^^^^^^^^ +The transaction builder is mutable. We add items to it using the ``TransactionBuilder.withItems`` method: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/transactions/TransactionBuilder.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +``withItems`` takes a ``vararg`` of objects and adds them to the builder based on their type: + +* ``StateAndRef`` objects are added as input states +* ``TransactionState`` and ``ContractState`` objects are added as output states +* ``Command`` objects are added as commands + +Passing in objects of any other type will cause an ``IllegalArgumentException`` to be thrown. + +You can also add the following items to the transaction: + +* ``TimeWindow`` objects, using ``TransactionBuilder.setTime`` +* ``SecureHash`` objects referencing the hash of an attachment stored on the node, using + ``TransactionBuilder.addAttachment`` + +Input states +~~~~~~~~~~~~ +Input states are added to a transaction as ``StateAndRef`` instances, rather than as ``ContractState`` instances. + +A ``StateAndRef`` combines a ``ContractState`` with a pointer to the transaction that created it. This series of +pointers from the input states back to the transactions that created them is what allows a node to work backwards and +verify the entirety of the transaction chain. It is defined as: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 7 + :end-before: DOCEND 7 + +Where ``StateRef`` is defined as: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 8 + :end-before: DOCEND 8 + +``StateRef.index`` is the state's position in the outputs of the transaction that created it. In this way, a +``StateRef`` allows a notary service to uniquely identify the existing states that a transaction is marking as historic. + +Output states +~~~~~~~~~~~~~ +Since a transaction's output states do not exist until the transaction is committed, they cannot be referenced as the +outputs of previous transactions. Instead, we create the desired output states as ``ContractState`` instances, and +add them to the transaction. + +Commands +~~~~~~~~ +Commands are added to the transaction as ``Command`` instances. ``Command`` combines a ``CommandData`` +instance representing the type of the command with a list of the command's required signers. It is defined as: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/contracts/Structures.kt + :language: kotlin + :start-after: DOCSTART 9 + :end-before: DOCEND 9 + +Signing the builder +^^^^^^^^^^^^^^^^^^^ +Once the builder is ready, we finalize it by signing it and converting it into a ``SignedTransaction``: + +.. container:: codeset + + .. sourcecode:: kotlin + + // Finalizes the builder by signing it with our primary signing key. + val signedTx1 = serviceHub.signInitialTransaction(unsignedTx) + + // Finalizes the builder by signing it with a different key. + val signedTx2 = serviceHub.signInitialTransaction(unsignedTx, otherKey) + + // Finalizes the builder by signing it with a set of keys. + val signedTx3 = serviceHub.signInitialTransaction(unsignedTx, otherKeys) + + .. sourcecode:: java + + // Finalizes the builder by signing it with our primary signing key. + final SignedTransaction signedTx1 = getServiceHub().signInitialTransaction(unsignedTx); + + // Finalizes the builder by signing it with a different key. + final SignedTransaction signedTx2 = getServiceHub().signInitialTransaction(unsignedTx, otherKey); + + // Finalizes the builder by signing it with a set of keys. + final SignedTransaction signedTx3 = getServiceHub().signInitialTransaction(unsignedTx, otherKeys); + +SignedTransaction +----------------- +A ``SignedTransaction`` is a combination of an immutable ``WireTransaction`` and a list of signatures over that +transaction: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +Verifying the signatures +^^^^^^^^^^^^^^^^^^^^^^^^ +The signatures on a ``SignedTransaction`` have not necessarily been checked for validity. We check them using +``SignedTransaction.verifySignatures``: + +.. container:: codeset + + .. literalinclude:: ../../core/src/main/kotlin/net/corda/core/transactions/SignedTransaction.kt + :language: kotlin + :start-after: DOCSTART 2 + :end-before: DOCEND 2 + +``verifySignatures`` takes a ``vararg`` of the public keys for which the signatures are allowed to be missing. If the +transaction is missing any signatures without the corresponding public keys being passed in, a +``SignaturesMissingException`` is thrown. + +Verifying the transaction +^^^^^^^^^^^^^^^^^^^^^^^^^ +Verifying a transaction is a multi-step process: + +* We check the transaction's signatures: + +.. container:: codeset + + .. sourcecode:: kotlin + + subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) + + .. sourcecode:: java + + subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); + +* Before verifying the transaction, we need to retrieve from the proposer(s) of the transaction any parts of the + transaction chain that our node doesn't currently have in its local storage: + +.. container:: codeset + + .. sourcecode:: kotlin + + subFlow(ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)) + + .. sourcecode:: java + + subFlow(new ResolveTransactionsFlow(transactionToVerify, partyWithTheFullChain)); + +* To verify the transaction, we first need to resolve any state references and attachment hashes by converting the + ``SignedTransaction`` into a ``LedgerTransaction``. We can then verify the fully-resolved transaction: + +.. container:: codeset + + .. sourcecode:: kotlin + + partSignedTx.tx.toLedgerTransaction(serviceHub).verify() + + .. sourcecode:: java + + partSignedTx.getTx().toLedgerTransaction(getServiceHub()).verify(); + +* We will generally also want to conduct some custom validation of the transaction, beyond what is provided for in the + contract: + +.. container:: codeset + + .. sourcecode:: kotlin + + val ledgerTransaction = partSignedTx.tx.toLedgerTransaction(serviceHub) + val inputStateAndRef = ledgerTransaction.inputs.single() + val input = inputStateAndRef.state.data as MyState + if (input.value > 1000000) { + throw FlowException("Proposed input value too high!") + } + + .. sourcecode:: java + + final LedgerTransaction ledgerTransaction = partSignedTx.getTx().toLedgerTransaction(getServiceHub()); + final StateAndRef inputStateAndRef = ledgerTransaction.getInputs().get(0); + final MyState input = (MyState) inputStateAndRef.getState().getData(); + if (input.getValue() > 1000000) { + throw new FlowException("Proposed input value too high!"); + } + +Signing the transaction +^^^^^^^^^^^^^^^^^^^^^^^ +We add an additional signature to an existing ``SignedTransaction`` using: + +.. container:: codeset + + .. sourcecode:: kotlin + + val fullySignedTx = serviceHub.addSignature(partSignedTx) + + .. sourcecode:: java + + SignedTransaction fullySignedTx = getServiceHub().addSignature(partSignedTx); + +We can also generate a signature over the transaction without adding it to the transaction directly by using: + +.. container:: codeset + + .. sourcecode:: kotlin + + val signature = serviceHub.createSignature(partSignedTx) + + .. sourcecode:: java + + DigitalSignature.WithKey signature = getServiceHub().createSignature(partSignedTx); + +Notarising and recording +^^^^^^^^^^^^^^^^^^^^^^^^ +Notarising and recording a transaction is handled by a built-in flow called ``FinalityFlow``. See +:doc:`api-flows` for more details. \ No newline at end of file diff --git a/docs/source/azure-vm.rst b/docs/source/azure-vm.rst index 50f94d072e..bc5ae24fa4 100644 --- a/docs/source/azure-vm.rst +++ b/docs/source/azure-vm.rst @@ -59,7 +59,7 @@ Define the version of Corda you want on your nodes and the type of notary. .. image:: resources/azure_multi_node_step3.png :width: 300px - + Click 'OK' STEP 4: Summary @@ -146,7 +146,7 @@ In the browser window type the following URL to send a Yo message to a target no .. sourcecode:: shell http://(public IP address):(port)/api/yo/yo?target=(legalname of target node) - + 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: .. sourcecode:: shell @@ -197,7 +197,7 @@ You can open log files with any text editor. .. image:: resources/azure_syslog.png :width: 300px - + Next Steps ---------- Now you have built a Corda network and used a basic Corda CorDapp do go and visit the `dedicated Corda website `_ diff --git a/docs/source/building-a-cordapp-index.rst b/docs/source/building-a-cordapp-index.rst new file mode 100644 index 0000000000..d5a3137e7d --- /dev/null +++ b/docs/source/building-a-cordapp-index.rst @@ -0,0 +1,10 @@ +Building a CorDapp +================== + +.. toctree:: + :maxdepth: 1 + + cordapp-overview + writing-cordapps + api-index + cheat-sheet \ No newline at end of file diff --git a/docs/source/changelog.rst b/docs/source/changelog.rst index 446dc4a903..b9e9aba247 100644 --- a/docs/source/changelog.rst +++ b/docs/source/changelog.rst @@ -7,75 +7,117 @@ from the previous milestone release. UNRELEASED ---------- -* API changes: +* A new RPC has been added to support fuzzy matching of X.500 names, for instance, to translate from user input to + an unambiguous identity by searching the network map. + +* The node driver has moved to net.corda.testing.driver in the test-utils module + +* Enable certificate validation in most scenarios (will be enforced in all cases in an upcoming milestone) + +* Added DER encoded format for CompositeKey so they can be used in X.509 certificates + +* Corrected several tests which made assumptions about counterparty keys, which are invalid when confidential identities + are used + +Milestone 13 +------------ + + * Web API related collections ``CordaPluginRegistry.webApis`` and ``CordaPluginRegistry.staticServeDirs`` moved to + ``net.corda.webserver.services.WebServerPluginRegistry`` in ``webserver`` module. + Classes serving Web API should now extend ``WebServerPluginRegistry`` instead of ``CordaPluginRegistry`` + and they should be registered in ``resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry``. + +Milestone 12 +------------ + +* Quite a few changes have been made to the flow API which should make things simpler when writing CorDapps: + * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with - ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``. + ``@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``. + * ``CordaPluginRegistry.servicePlugins`` is also no longer used, along with ``PluginServiceHub.registerFlowInitiator``. + Instead annotate your initiated flows with ``@InitiatedBy``. This annotation takes a single parameter which is the + initiating flow. This initiating flow further has to be annotated with ``@InitiatingFlow``. For any services you + may have, such as oracles, annotate them with ``@CordaService``. These annotations will be picked up automatically + when the node starts up. - * ``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 - ``IllegalArgumentException`` if the initiating flow class is not annotated with it. + * Due to these changes, when unit testing flows make sure to use ``AbstractNode.registerInitiatedFlow`` so that the flows + are wired up. Likewise for services use ``AbstractNode.installCordaService``. - * Also related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been - removed. Its purpose was to allow subflows to be inlined with the parent flow - i.e. the subflow does not initiate - new sessions with parties the parent flow has already started. This allowed flows to be used as building blocks. To - achieve the same effect now simply requires the subflow to be *not* annotated wth ``InitiatingFlow`` (i.e. we've made - this the default behaviour). If the subflow is not meant to be inlined, and is supposed to initiate flows on the - other side, the annotation is required. + * Related to ``InitiatingFlow``, the ``shareParentSessions`` boolean parameter of ``FlowLogic.subFlow`` has been + removed. This was an unfortunate parameter that unnecessarily exposed the inner workings of flow sessions. Now, if + your sub-flow can be started outside the context of the parent flow then annotate it with ``@InitiatingFlow``. If + it's meant to be used as a continuation of the existing parent flow, such as ``CollectSignaturesFlow``, then it + doesn't need any annotation. - * ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``. + * The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version + number, defaulting to 1 if it's not specified. This enables versioning of flows with nodes only accepting communication + if the version number matches. At some point we will support the ability for a node to have multiple versions of the + same flow registered, enabling backwards compatibility of flows. - * ``NotaryChangeFlow.Instigator`` has been renamed to just ``NotaryChangeFlow``. + * ``ContractUpgradeFlow.Instigator`` has been renamed to just ``ContractUpgradeFlow``. - * ``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. + * ``NotaryChangeFlow.Instigator`` has been renamed to just ``NotaryChangeFlow``. - * There are major changes to the ``Party`` class as part of confidential identities: + * ``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. - * ``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. 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 the ``Party`` class as part of confidential identities: - * There are major changes to transaction signing in flows: + * ``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. This now replaces + use of ``Party`` and ``PublicKey`` in state objects, and allows use of full or anonymised parties depending on + use-case. + * A new ``PartyAndCertificate`` class has been added which aggregates a Party along with an X.509 certificate and + certificate path back to a network trust root. This is used where a Party and its proof of identity are required, + for example in identity registration. + * 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. - * You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most +* The identity management service takes an optional network trust root which it will validate certificate paths to, if + provided. A later release will make this a required parameter. + +* 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 + 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. + * The ``KeyManagementService`` no longer provides any 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 keypairs 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. + +* ``Timestamp`` used for validation/notarization time-range has been renamed to ``TimeWindow``. + There are now 4 factory methods ``TimeWindow.fromOnly(fromTime: Instant)``, + ``TimeWindow.untilOnly(untilTime: Instant)``, ``TimeWindow.between(fromTime: Instant, untilTime: Instant)`` and + ``TimeWindow.withTolerance(time: Instant, tolerance: Duration)``. + Previous constructors ``TimeWindow(fromTime: Instant, untilTime: Instant)`` and + ``TimeWindow(time: Instant, tolerance: Duration)`` have been removed. + +* The Bouncy Castle library ``X509CertificateHolder`` class is now used in place of ``X509Certificate`` in order to + have a consistent class used internally. Conversions to/from ``X509Certificate`` are done as required, but should + be avoided where possible. * The certificate hierarchy has been changed in order to allow corda node to sign keys with proper certificate chain. * The corda node will now be issued a restricted client CA for identity/transaction key signing. * TLS certificate are now stored in `sslkeystore.jks` and identity keys are stored in `nodekeystore.jks` + .. warning:: The old keystore will need to be removed when upgrading to this version. Milestone 11.1 @@ -513,7 +555,7 @@ New features in this release: are trees of public keys in which interior nodes can have validity thresholds attached, thus allowing boolean formulas of keys to be created. This is similar to Bitcoin's multi-sig support and the data model is the same as the InterLedger Crypto-Conditions spec, which should aid interop in future. Read more about - key trees in the ":doc:`key-concepts-core-types`" article. + key trees in the ":doc:`api-core-types`" article. * A new tutorial has been added showing how to use transaction attachments in more detail. * Testnet @@ -529,7 +571,7 @@ New features in this release: * Standalone app development: * The Corda libraries that app developers need to link against can now be installed into your local Maven - repository, where they can then be used like any other JAR. See :doc:`creating-a-cordapp`. + repository, where they can then be used like any other JAR. See :doc:`running-a-node`. * User interfaces: @@ -688,8 +730,8 @@ Highlights of this release: We have new documentation on: * :doc:`event-scheduling` -* :doc:`key-concepts-core-types` -* :doc:`key-concepts-consensus-notaries` +* :doc:`core-types` +* :doc:`key-concepts-consensus` Summary of API changes (not exhaustive): diff --git a/docs/source/clientrpc.rst b/docs/source/clientrpc.rst index 82def31fed..38e8661c82 100644 --- a/docs/source/clientrpc.rst +++ b/docs/source/clientrpc.rst @@ -86,7 +86,7 @@ Whitelisting classes with the Corda node To avoid the RPC interface being wide open to all classes on the classpath, Cordapps have to whitelist any classes they require with the serialization framework of Corda, if they are not one of those whitelisted by default in ``DefaultWhitelist``, via either the plugin architecture or simply -with the annotation ``@CordaSerializable``. See :doc:`creating-a-cordapp` or :doc:`serialization`. An example is shown in :doc:`tutorial-clientrpc-api`. +with the annotation ``@CordaSerializable``. See :doc:`running-a-node` or :doc:`serialization`. An example is shown in :doc:`tutorial-clientrpc-api`. .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. diff --git a/docs/source/component-library-index.rst b/docs/source/component-library-index.rst new file mode 100644 index 0000000000..415b81b71b --- /dev/null +++ b/docs/source/component-library-index.rst @@ -0,0 +1,10 @@ +Component library +================= + +.. toctree:: + :maxdepth: 1 + + flow-library + contract-catalogue + financial-model + contract-irs \ No newline at end of file diff --git a/docs/source/contract-irs.rst b/docs/source/contract-irs.rst index 195b9fafd0..f8e898c4b2 100644 --- a/docs/source/contract-irs.rst +++ b/docs/source/contract-irs.rst @@ -33,7 +33,7 @@ the view of the floating leg receiver / fixed leg payer. The enumerated document it progresses (note that, the first version exists before the value date), the dots on the "y=0" represent an interest rate value becoming available and then the curved arrow indicates to which period the fixing applies. -.. image:: contract-irs.png +.. image:: resources/contract-irs.png Two days (by convention, although this can be modified) before the value date (i.e. at the start of the swap) in the red period, the reference rate is observed from an oracle and fixed - in this instance, at 1.1%. At the end of the accrual period, diff --git a/docs/source/contract-upgrade.rst b/docs/source/contract-upgrade.rst index ac3600165a..e448ade570 100644 --- a/docs/source/contract-upgrade.rst +++ b/docs/source/contract-upgrade.rst @@ -4,7 +4,7 @@ -Upgrading Contracts +Upgrading contracts =================== While every care is taken in development of contract code, diff --git a/docs/source/corda-configuration-file.rst b/docs/source/corda-configuration-file.rst index dcfa211d34..fb8c078b9e 100644 --- a/docs/source/corda-configuration-file.rst +++ b/docs/source/corda-configuration-file.rst @@ -34,14 +34,13 @@ NetworkMapService plus Simple Notary configuration file. .. parsed-literal:: - myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity : "London" + myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "localhost:12345" rpcAddress : "localhost:12346" webAddress : "localhost:12347" - extraAdvertisedServiceIds : [] + extraAdvertisedServiceIds : ["corda.notary.simple"] useHTTPS : false devMode : true // Certificate signing service will be hosted by R3 in the near future. @@ -56,9 +55,6 @@ path to the node's base directory. :myLegalName: The legal identity of the node acts as a human readable alias to the node's public key and several demos use this to lookup the NodeInfo. -:nearestCity: The location of the node as used to locate coordinates on the world map when running the network simulator - demo. See :doc:`network-simulator`. - :keyStorePassword: The password to unlock the KeyStore file (``/certificates/sslkeystore.jks``) containing the node certificate and private key. @@ -104,7 +100,7 @@ path to the node's base directory. :notaryNodeAddress: The host and port to which to bind the embedded Raft server. Required only when running a distributed notary service. A group of Corda nodes can run a distributed notary service by each running an embedded Raft server and joining them to the same cluster to replicate the committed state log. Note that the Raft cluster uses a separate transport - layer for communication that does not integrate with ArtemisMQ messaging services. + layer for communication that does not integrate with ArtemisMQ messaging services. :notaryClusterAddresses: List of Raft cluster member addresses used to join the cluster. At least one of the specified members must be active and be able to communicate with the cluster leader for joining. If empty, a new cluster will be diff --git a/docs/source/corda-networks-index.rst b/docs/source/corda-networks-index.rst new file mode 100644 index 0000000000..abdcf7c0b5 --- /dev/null +++ b/docs/source/corda-networks-index.rst @@ -0,0 +1,9 @@ +Corda networks +============== + +.. toctree:: + :maxdepth: 1 + + setting-up-a-corda-network + permissioning + versioning \ No newline at end of file diff --git a/docs/source/corda-nodes-index.rst b/docs/source/corda-nodes-index.rst new file mode 100644 index 0000000000..0297f1c744 --- /dev/null +++ b/docs/source/corda-nodes-index.rst @@ -0,0 +1,12 @@ +Corda nodes +=========== + +.. toctree:: + :maxdepth: 1 + + running-a-node + clientrpc + shell + node-administration + corda-configuration-file + out-of-process-verification \ No newline at end of file diff --git a/docs/source/corda-plugins.rst b/docs/source/corda-plugins.rst deleted file mode 100644 index 75a893449a..0000000000 --- a/docs/source/corda-plugins.rst +++ /dev/null @@ -1,87 +0,0 @@ -The Corda plugin framework -========================== - -The intention is that Corda is a common platform, which will be extended -by numerous application extensions (CorDapps). These extensions will -package together all of the Corda contract code, state structures, -protocols/flows to create and modify state as well as RPC extensions for -node clients. Details of writing these CorDapps is given elsewhere -:doc:`creating-a-cordapp`. - -To enable these plugins to register dynamically with the Corda framework -the node uses the Java ``ServiceLoader`` to locate and load the plugin -components during the ``AbstractNode.start`` call. Therefore, -to be recognised as a plugin the component must: - -1. Include a default constructable class extending from -``net.corda.core.node.CordaPluginRegistry`` which overrides the relevant -registration methods. - -2. Include a resource file named -``net.corda.core.node.CordaPluginRegistry`` in the ``META-INF.services`` -path. This must include a line containing the fully qualified name of -the ``CordaPluginRegistry`` implementation class. Multiple plugin -registries are allowed in this file if desired. - -3. The plugin component must be on the classpath. In the normal use this -means that it should be present within the plugins subfolder of the -node's workspace. - -4. As a plugin the registered components are then allowed access to some -of the node internal subsystems. - -5. The overridden properties on the registry class information about the different -extensions to be created, or registered at startup. In particular: - - a. The ``webApis`` property is a list of JAX-RS annotated REST access - classes. These classes will be constructed by the bundled web server - and must have a single argument constructor taking a ``CordaRPCOps`` - reference. This will allow it to communicate with the node process - via the RPC interface. These web APIs will not be available if the - bundled web server is not started. - - b. The ``staticServeDirs`` property maps static web content to virtual - paths and allows simple web demos to be distributed within the CorDapp - jars. These static serving directories will not be available if the - bundled web server is not started. - - 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 - ``SingletonSerializeAsToken`` which ensures that if any reference to your - service is captured in a flow checkpoint (i.e. serialized by Kryo as - part of Quasar checkpoints, either on the stack or by reference within - your flows) it is stored as a simple token representing your service. - When checkpoints are restored, after a node restart for example, - the latest instance of the service will be substituted back in place of - the token stored in the checkpoint. - - i. Firstly, they can call ``PluginServiceHub.registerServiceFlow`` and - register flows that will be initiated locally in response to remote flow - requests. - - ii. Second, the service can hold a long lived reference to the - PluginServiceHub and to other private data, so the service can be used - to provide Oracle functionality. This Oracle functionality would - typically be exposed to other nodes by flows which are given a reference - to the service plugin when initiated (as defined by the - ``registerServiceFlow`` call). The flow can then call into functions - on the plugin service singleton. Note, care should be taken to not allow - flows to hold references to fields which are not - also ``SingletonSerializeAsToken``, otherwise Quasar suspension in the - ``StateMachineManager`` will fail with exceptions. An example oracle can - be seen in ``NodeInterestRates.kt`` in the irs-demo sample. - - iii. The final use case for service plugins is that they can spawn threads, or register - to monitor vault updates. This allows them to provide long lived active - functions inside the node, for instance to initiate workflows when - certain conditions are met. - - 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 - various points on various threads and needs to be stable and thread safe. See - :doc:`serialization`. - diff --git a/docs/source/corda-repo-layout.rst b/docs/source/corda-repo-layout.rst new file mode 100644 index 0000000000..0c95bb74f2 --- /dev/null +++ b/docs/source/corda-repo-layout.rst @@ -0,0 +1,30 @@ +Corda repo layout +================= + +The Corda repository comprises the following folders: + +* **buildSrc** contains necessary gradle plugins to build Corda +* **client** contains libraries for connecting to a node, working with it remotely and binding server-side data to + JavaFX UI +* **config** contains logging configurations and the default node configuration file +* **cordform-common** contains utilities related to building and running nodes +* **core** containing the core Corda libraries such as crypto functions, types for Corda's building blocks: states, + contracts, transactions, attachments, etc. and some interfaces for nodes and protocols +* **docs** contains the Corda docsite in restructured text format as well as the built docs in html. The docs can be + accessed via ``/docs/index.html`` from the root of the repo +* **experimental** contains platform improvements that are still in the experimental stage +* **finance** defines a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible + assets, cash, obligation and commercial paper +* **gradle** contains the gradle wrapper which you'll use to execute gradle commands +* **gradle-plugins** contains some additional plugins which we use to deploy Corda nodes +* **lib** contains some dependencies +* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence) +* **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC +* **node-schemas** contains entity classes used to represent relational database tables +* **samples** contains all our Corda demos and code samples +* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the + mock network) implementation +* **tools** contains the explorer which is a GUI front-end for Corda, and also the DemoBench which is a GUI tool that + allows you to run Corda nodes locally for demonstrations +* **verifier** allows out-of-node transaction verification, allowing verification to scale horizontally +* **webserver** is a servlet container for CorDapps that export HTTP endpoints. This server is an RPC client of the node \ No newline at end of file diff --git a/docs/source/cordapp-overview.rst b/docs/source/cordapp-overview.rst new file mode 100644 index 0000000000..ee4656d872 --- /dev/null +++ b/docs/source/cordapp-overview.rst @@ -0,0 +1,26 @@ +What is a CorDapp? +================== + +Corda is a platform. Its functionality is extended by developers through the writing of Corda distributed +applications (CorDapps). CorDapps are installed at the level of the individual node, rather than on the network +itself. + +Each CorDapp allows a node to handle new business processes - everything from asset trading (see :ref:`irs-demo`) to +portfolio valuations (see :ref:`simm-demo`). It does so by defining new flows on the node that, once started by the +node owner, conduct the process of negotiating a specific ledger update with other nodes on the network. The node's +owner can then start these flows as required, either through remote procedure calls (RPC) or HTTP requests that +leverage the RPC interface. + +.. image:: resources/node-diagram.png + +CorDapp developers will usually define not only these flows, but also any states and contracts that these flows use. +They will also have to define any web APIs that will run on the node's standalone web server, any static web content, +and any new services that they want their CorDapp to offer. + +CorDapps are made up of definitions for the following components: + +* States +* Contracts +* Flows +* Web APIs and static web content +* Services \ No newline at end of file diff --git a/docs/source/demobench.rst b/docs/source/demobench.rst index 1fa649a913..5382547301 100644 --- a/docs/source/demobench.rst +++ b/docs/source/demobench.rst @@ -62,7 +62,7 @@ DemoBench writes a log file to the following location: Building the Installers ----------------------- -There are three scripts in the ``tools/demobench`` directory: +Gradle defines tasks that build DemoBench installers using JavaPackager. There are three scripts in the ``tools/demobench`` directory to execute these tasks: #. ``package-demobench-exe.bat`` (Windows) #. ``package-demobench-dmg.sh`` (MacOS) @@ -71,11 +71,36 @@ There are three scripts in the ``tools/demobench`` directory: Each script can only be run on its target platform, and each expects the platform's installation tools already to be available. #. Windows: `Inno Setup 5+ `_ - #. MacOS: The packaging tools should be available automatically. The DMG contents will also be signed if the packager finds a valid ``Developer ID Application`` certificate on the keyring. You can create such a certificate by generating a Certificate Signing Request and then asking your local "Apple team agent" to upload it to the Apple Developer portal. (See `here `_.) - #. Fedora/Linux: ``rpm-build`` packages. + + #. MacOS: The packaging tools should be available automatically. The DMG contents will also be signed if the packager finds a valid ``Developer ID Application`` certificate with a private key on the keyring. (By default, DemoBench's ``build.gradle`` expects the signing key's user name to be "R3CEV".) You can create such a certificate by generating a Certificate Signing Request and then asking your local "Apple team agent" to upload it to the Apple Developer portal. (See `here `_.) + +.. note:: + + - Please ensure that the ``/usr/bin/codesign`` application always has access to your certificate's signing key. You may need to reboot your Mac after making any changes via the MacOS Keychain Access application. + + - You should use JDK >= 8u152 to build DemoBench on MacOS because this version resolves a warning message that is printed to the terminal when starting each Corda node. + + - Ideally, use the :ref:`jetbrains-jdk` to build the DMG. + +.. + + 3. Fedora/Linux: ``rpm-build`` packages. You will also need to define the environment variable ``JAVA_HOME`` to point to the same JDK that you use to run Gradle. The installer will be written to the ``tools/demobench/build/javapackage/bundles`` directory, and can be installed like any other application for your platform. +.. _jetbrains-jdk: + +JetBrains JDK +------------- + +Mac users should note that the best way to build a DemoBench DMG is with the `JetBrains JDK `_ +which has `binary downloads available from BinTray `_. +This JDK has some useful GUI fixes, most notably, when built with this JDK the DemoBench terminal will support emoji +and as such, the nicer coloured ANSI progress renderer. It also resolves some issues with HiDPI rendering on +Windows. + +This JDK does not include JavaPackager, which means that you will still need to copy ``$JAVA_HOME/lib/ant-javafx.jar`` from an Oracle JDK into the corresponding directory within your JetBrains JDK. + Developer Notes --------------- @@ -89,7 +114,6 @@ Developers wishing to run DemoBench *without* building a new installer each time .. - Unfortunately, DemoBench's ``$CLASSPATH`` may be too long for the Windows shell . In which case you can still run DemoBench as follows: .. parsed-literal:: @@ -98,7 +122,9 @@ Unfortunately, DemoBench's ``$CLASSPATH`` may be too long for the Windows shell .. -While DemoBench *can* be executed within an IDE, it would be up to the Developer to install all of its runtime dependencies beforehand into their correct locations relative to the value of the ``user.dir`` system property (i.e. the current working directory of the JVM): +While DemoBench *can* be executed within an IDE, it would be up to the Developer to install all of its runtime +dependencies beforehand into their correct locations relative to the value of the ``user.dir`` system property (i.e. the +current working directory of the JVM): .. parsed-literal:: diff --git a/docs/source/example-code/build.gradle b/docs/source/example-code/build.gradle index 311bf42623..fe708b84ad 100644 --- a/docs/source/example-code/build.gradle +++ b/docs/source/example-code/build.gradle @@ -35,16 +35,16 @@ compileTestJava.dependsOn tasks.getByPath(':node:capsule:buildCordaJAR') dependencies { compile project(':core') compile project(':client:jfx') - compile project(':node') - testCompile project(':test-utils') + compile project(':test-utils') + testCompile project(':verifier') compile "org.graphstream:gs-core:1.3" compile("org.graphstream:gs-ui:1.3") { exclude group: "bouncycastle" } - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') } mainClassName = "net.corda.docs.ClientRpcTutorialKt" @@ -70,10 +70,9 @@ task integrationTest(type: Test) { task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 @@ -81,8 +80,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { cordapps = [] } node { - name "CN=Alice Corp,O=Alice Corp,L=London,C=UK" - nearestCity "London" + name "CN=Alice Corp,O=Alice Corp,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 diff --git a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt index c6c8afb8f8..af5ff519cd 100644 --- a/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt +++ b/docs/source/example-code/src/integration-test/kotlin/net/corda/docs/IntegrationTestingTutorial.kt @@ -14,7 +14,7 @@ import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow -import net.corda.node.driver.driver +import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User diff --git a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt index b232f9fd6e..dd870c27d0 100644 --- a/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt +++ b/docs/source/example-code/src/main/kotlin/net/corda/docs/ClientRpcTutorial.kt @@ -17,10 +17,10 @@ import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.CashExitFlow import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow -import net.corda.node.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.nodeapi.User +import net.corda.testing.driver.driver import org.graphstream.graph.Edge import org.graphstream.graph.Node import org.graphstream.graph.implementations.MultiGraph 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 4a7def4e87..39b61b1b09 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 @@ -9,8 +9,10 @@ import net.corda.core.contracts.TransactionType import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.PluginServiceHub import net.corda.core.node.ServiceHub import net.corda.core.node.services.unconsumedStates @@ -21,13 +23,6 @@ import net.corda.flows.FinalityFlow import net.corda.flows.ResolveTransactionsFlow import java.util.* -object FxTransactionDemoTutorial { - // Would normally be called by custom service init in a CorDapp - fun registerFxProtocols(pluginHub: PluginServiceHub) { - pluginHub.registerServiceFlow(ForeignExchangeFlow::class.java, ::ForeignExchangeRemoteFlow) - } -} - @CordaSerializable private data class FxRequest(val tradeId: String, val amount: Amount>, @@ -212,6 +207,7 @@ class ForeignExchangeFlow(val tradeId: String, // DOCEND 3 } +@InitiatedBy(ForeignExchangeFlow::class) class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic() { @Suspendable override fun call() { 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 6c25dea3b7..43c7b5b245 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 @@ -6,10 +6,10 @@ 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.InitiatedBy 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 import net.corda.core.node.services.linearHeadsOfType import net.corda.core.serialization.CordaSerializable @@ -19,22 +19,13 @@ import net.corda.flows.FinalityFlow import java.security.PublicKey import java.time.Duration -object WorkflowTransactionBuildTutorial { - // Would normally be called by custom service init in a CorDapp - fun registerWorkflowProtocols(pluginHub: PluginServiceHub) { - pluginHub.registerServiceFlow(SubmitCompletionFlow::class.java, ::RecordCompletionFlow) - } -} - // DOCSTART 1 - // Helper method to locate the latest Vault version of a LinearState from a possibly out of date StateRef inline fun ServiceHub.latest(ref: StateRef): StateAndRef { val linearHeads = vaultService.linearHeadsOfType() val original = toStateAndRef(ref) - return linearHeads.get(original.state.data.linearId)!! + return linearHeads[original.state.data.linearId]!! } - // DOCEND 1 // Minimal state model of a manual approval process @@ -80,14 +71,14 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash */ override fun verify(tx: TransactionForContract) { val command = tx.commands.requireSingleCommand() - require(tx.timestamp?.midpoint != null) { "must be timestamped" } + require(tx.timeWindow?.midpoint != null) { "must have a time-window" } when (command.value) { is Commands.Issue -> { requireThat { "Issue of new WorkflowContract must not include any inputs" using (tx.inputs.isEmpty()) "Issue of new WorkflowContract must be in a unique transaction" using (tx.outputs.size == 1) } - val issued = tx.outputs.get(0) as TradeApprovalContract.State + val issued = tx.outputs[0] as TradeApprovalContract.State requireThat { "Issue requires the source Party as signer" using (command.signers.contains(issued.source.owningKey)) "Initial Issue state must be NEW" using (issued.state == WorkflowState.NEW) @@ -96,9 +87,9 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash is Commands.Completed -> { val stateGroups = tx.groupStates(TradeApprovalContract.State::class.java) { it.linearId } require(stateGroups.size == 1) { "Must be only a single proposal in transaction" } - for (group in stateGroups) { - val before = group.inputs.single() - val after = group.outputs.single() + for ((inputs, outputs) in stateGroups) { + val before = inputs.single() + val after = outputs.single() requireThat { "Only a non-final trade can be modified" using (before.state == WorkflowState.NEW) "Output must be a final state" using (after.state in setOf(WorkflowState.APPROVED, WorkflowState.REJECTED)) @@ -132,7 +123,7 @@ class SubmitTradeApprovalFlow(val tradeId: String, // Create the TransactionBuilder and populate with the new state. val tx = TransactionType.General.Builder(notary) .withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey))) - tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.addTimeWindow(serviceHub.clock.instant(), Duration.ofSeconds(60)) // We can automatically sign as there is no untrusted data. val signedTx = serviceHub.signInitialTransaction(tx) // Notarise and distribute. @@ -193,7 +184,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow newState, Command(TradeApprovalContract.Commands.Completed(), listOf(serviceHub.myInfo.legalIdentity.owningKey, latestRecord.state.data.source.owningKey))) - tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) + tx.addTimeWindow(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. // As a SignedTransaction we can pass the data around certain that it cannot be modified, @@ -227,6 +218,7 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow * Then after checking to sign it and eventually store the fully notarised * transaction to the ledger. */ +@InitiatedBy(SubmitCompletionFlow::class) class RecordCompletionFlow(val source: Party) : FlowLogic() { @Suspendable override fun call(): Unit { diff --git a/docs/source/example-code/src/main/resources/example-network-map-node.conf b/docs/source/example-code/src/main/resources/example-network-map-node.conf index 1cebb8b8c5..1b425c1bd7 100644 --- a/docs/source/example-code/src/main/resources/example-network-map-node.conf +++ b/docs/source/example-code/src/main/resources/example-network-map-node.conf @@ -1,5 +1,4 @@ -myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" p2pAddress : "my-network-map:10000" diff --git a/docs/source/example-code/src/main/resources/example-node.conf b/docs/source/example-code/src/main/resources/example-node.conf index de024c5215..bb7ce35947 100644 --- a/docs/source/example-code/src/main/resources/example-node.conf +++ b/docs/source/example-code/src/main/resources/example-node.conf @@ -1,5 +1,4 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" keyStorePassword : "cordacadevpass" trustStorePassword : "trustpass" dataSourceProperties : { @@ -14,7 +13,7 @@ webAddress : "localhost:10004" extraAdvertisedServiceIds : [ "corda.interest_rates" ] networkMapService : { address : "my-network-map:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK" + legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" } useHTTPS : false rpcUsers : [ diff --git a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf index f7636fc105..713da39f55 100644 --- a/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf +++ b/docs/source/example-code/src/main/resources/example-out-of-process-verifier-node.conf @@ -1,9 +1,8 @@ -myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK" -nearestCity : "London" +myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB" p2pAddress : "my-corda-node:10002" webAddress : "localhost:10003" networkMapService : { address : "my-network-map:10000" - legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK" + legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB" } verifierType: "OutOfProcess" 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 5713a3041a..4210c57ce6 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 @@ -18,31 +18,28 @@ import org.junit.Test import kotlin.test.assertEquals class FxTransactionBuildTutorialTest { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var nodeA: MockNetwork.MockNode lateinit var nodeB: MockNetwork.MockNode @Before fun setup() { - net = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true) val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = net.createNode( + notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, - overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), + overrideServices = mapOf(notaryService to DUMMY_NOTARY_KEY), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = net.createPartyNode(notaryNode.info.address) - nodeB = net.createPartyNode(notaryNode.info.address) - FxTransactionDemoTutorial.registerFxProtocols(nodeA.services) - FxTransactionDemoTutorial.registerFxProtocols(nodeB.services) - WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services) - WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services) + nodeA = mockNet.createPartyNode(notaryNode.info.address) + nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeB.registerInitiatedFlow(ForeignExchangeRemoteFlow::class.java) } @After fun cleanUp() { println("Close DB") - net.stopNodes() + mockNet.stopNodes() } @Test diff --git a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt index 6815a205e2..b456cb8e02 100644 --- a/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt +++ b/docs/source/example-code/src/test/kotlin/net/corda/docs/WorkflowTransactionBuildTutorialTest.kt @@ -4,7 +4,6 @@ import net.corda.core.contracts.LinearState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.getOrThrow -import net.corda.core.node.ServiceEntry import net.corda.core.node.ServiceHub import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.linearHeadsOfType @@ -21,7 +20,7 @@ import org.junit.Test import kotlin.test.assertEquals class WorkflowTransactionBuildTutorialTest { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var nodeA: MockNetwork.MockNode lateinit var nodeB: MockNetwork.MockNode @@ -30,29 +29,26 @@ class WorkflowTransactionBuildTutorialTest { private inline fun ServiceHub.latest(ref: StateRef): StateAndRef { val linearHeads = vaultService.linearHeadsOfType() val original = storageService.validatedTransactions.getTransaction(ref.txhash)!!.tx.outRef(ref.index) - return linearHeads.get(original.state.data.linearId)!! + return linearHeads[original.state.data.linearId]!! } @Before fun setup() { - net = MockNetwork(threadPerNode = true) + mockNet = MockNetwork(threadPerNode = true) val notaryService = ServiceInfo(ValidatingNotaryService.type) - notaryNode = net.createNode( + notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, overrideServices = mapOf(Pair(notaryService, DUMMY_NOTARY_KEY)), advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), notaryService)) - nodeA = net.createPartyNode(notaryNode.info.address) - nodeB = net.createPartyNode(notaryNode.info.address) - FxTransactionDemoTutorial.registerFxProtocols(nodeA.services) - FxTransactionDemoTutorial.registerFxProtocols(nodeB.services) - WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeA.services) - WorkflowTransactionBuildTutorial.registerWorkflowProtocols(nodeB.services) + nodeA = mockNet.createPartyNode(notaryNode.info.address) + nodeB = mockNet.createPartyNode(notaryNode.info.address) + nodeA.registerInitiatedFlow(RecordCompletionFlow::class.java) } @After fun cleanUp() { println("Close DB") - net.stopNodes() + mockNet.stopNodes() } @Test diff --git a/docs/source/faq.rst b/docs/source/faq.rst new file mode 100644 index 0000000000..0e30991baa --- /dev/null +++ b/docs/source/faq.rst @@ -0,0 +1,4 @@ +Frequently asked questions +========================== + +A list of frequently asked questions can be found here: ``_. diff --git a/docs/source/key-concepts-financial-model.rst b/docs/source/financial-model.rst similarity index 95% rename from docs/source/key-concepts-financial-model.rst rename to docs/source/financial-model.rst index f43a5b282d..b5f4e9d77c 100644 --- a/docs/source/key-concepts-financial-model.rst +++ b/docs/source/financial-model.rst @@ -7,8 +7,8 @@ These provide a common language for states and contracts. Amount ------ -The `Amount `_ class is used to represent an amount of some -fungible asset. It is a generic class which wraps around a type used to define the underlying product, called +The `Amount `_ class is used to represent an amount of +some fungible asset. It is a generic class which wraps around a type used to define the underlying product, called the *token*. For instance it can be the standard JDK type ``Currency``, or an ``Issued`` instance, or this can be a more complex type such as an obligation contract issuance definition (which in turn contains a token definition for whatever the obligation is to be settled in). Custom token types should implement ``TokenizableAssetInfo`` to allow the diff --git a/docs/source/flow-library.rst b/docs/source/flow-library.rst index 1110dbc813..d6633f50b1 100644 --- a/docs/source/flow-library.rst +++ b/docs/source/flow-library.rst @@ -1,4 +1,4 @@ -Flow Library +Flow library ============ There are a number of built-in flows supplied with Corda, which cover some core functionality. diff --git a/docs/source/flow-state-machines.rst b/docs/source/flow-state-machines.rst index 9e90f045cd..f03d751ab5 100644 --- a/docs/source/flow-state-machines.rst +++ b/docs/source/flow-state-machines.rst @@ -89,9 +89,10 @@ Our flow has two parties (B and S for buyer and seller) and will proceed as foll 2. B sends to S a ``SignedTransaction`` that includes the state as input, B's cash as input, the state with the new owner key as output, and any change cash as output. It contains a single signature from B but isn't valid because it lacks a signature from S authorising movement of the asset. -3. S signs it and *finalises* the transaction. This means sending it to the notary, which checks the transaction for - validity, recording the transaction in the local vault, and then sending it back to B who also checks it and commits - the transaction to their local vault. +3. S signs the transaction and sends it back to B. +4. B *finalises* the transaction by sending it to the notary who checks the transaction for validity, + recording the transaction in B's local vault, and then sending it on to S who also checks it and commits + the transaction to S's local vault. You can find the implementation of this flow in the file ``finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt``. @@ -124,9 +125,6 @@ each side. val sellerOwnerKey: PublicKey ) - data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey, - val notarySig: DigitalSignature.LegallyIdentifiable) - open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, @@ -156,7 +154,7 @@ simply flow messages or exceptions. The other two represent the buyer and seller Going through the data needed to become a seller, we have: - ``otherParty: Party`` - the party with which you are trading. -- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`key-concepts-consensus-notaries`" for more +- ``notaryNode: NodeInfo`` - the entry in the network map for the chosen notary. See ":doc:`key-concepts-notaries`" for more 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). @@ -243,87 +241,111 @@ Let's implement the ``Seller.call`` method. This will be run when the flow is in :end-before: DOCEND 4 :dedent: 4 -Here we see the outline of the procedure. We receive a proposed trade transaction from the buyer and check that it's -valid. The buyer has already attached their signature before sending it. Then we calculate and attach our own signature -so that the transaction is now signed by both the buyer and the seller. We then *finalise* this transaction by sending -it to a notary to assert (with another signature) that the timestamp in the transaction (if any) is valid and there are no -double spends. Finally, after the finalisation process is complete, we retrieve the now fully signed transaction from -local storage. It will have the same ID as the one we started with but more signatures. +We start by sending information about the asset we wish to sell to the buyer. We fill out the initial flow message with +the trade info, and then call ``send``. which takes two arguments: -Let's fill out the ``receiveAndCheckProposedTransaction()`` method. +- The party we wish to send the message to. +- The payload being sent. + +``send`` will serialise the payload and send it to the other party automatically. + +Next, we call a *subflow* called ``SignTransactionFlow`` (see :ref:`subflows`). ``SignTransactionFlow`` automates the +process of: + +* Receiving a proposed trade transaction from the buyer, with the buyer's signature attached. +* Checking that the proposed transaction is valid. +* Calculating and attaching our own signature so that the transaction is now signed by both the buyer and the seller. +* Sending the transaction back to the buyer. + +The transaction then needs to be finalized. This is the the process of sending the transaction to a notary to assert +(with another signature) that the timestamp in the transaction (if any) is valid and there are no double spends. +In this flow, finalization is handled by the buyer, so we just wait for the signed transaction to appear in our +transaction storage. It will have the same ID as the one we started with but more signatures. + +Implementing the buyer +---------------------- + +OK, let's do the same for the buyer side: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 5 - :end-before: DOCEND 5 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + :dedent: 4 -Let's break this down. We fill out the initial flow message with the trade info, and then call ``sendAndReceive``. -This function takes a few arguments: +This code is longer but no more complicated. Here are some things to pay attention to: -- The party on the other side. -- The thing to send. It'll be serialised and sent automatically. -- Finally a type argument, which is the kind of object we're expecting to receive from the other side. If we get - back something else an exception is thrown. +1. We do some sanity checking on the proposed trade transaction received from the seller to ensure we're being offered + what we expected to be offered. +2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this. +3. We access the *service hub* as needed to access things that are transient and may change or be recreated + whilst a flow is suspended, such as the wallet or the network map. +4. We call ``CollectSignaturesFlow`` as a subflow to send the unfinished, still-invalid transaction to the seller so + they can sign it and send it back to us. +5. Last, we call ``FinalityFlow`` as a subflow to finalize the transaction. -Once ``sendAndReceive`` is called, the call method will be suspended into a continuation and saved to persistent -storage. If the node crashes or is restarted, the flow will effectively continue as if nothing had happened. Your -code may remain blocked inside such a call for seconds, minutes, hours or even days in the case of a flow that -needs human interaction! +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. -.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation. - The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by - the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to. You can - always use private methods to keep the stack uncluttered with temporary variables, or to avoid objects that - Kryo is not able to serialise correctly. +Flow sessions +------------- - The second is that as well as being kept on the heap, objects reachable from the stack will be serialised. The state - of the function call may be resurrected much later! Kryo doesn't require objects be marked as serialisable, but even so, - doing things like creating threads from inside these calls would be a bad idea. They should only contain business - logic and only do I/O via the methods exposed by the flow framework. +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. - It's OK to keep references around to many large internal node services though: these will be serialised using a - special token that's recognised by the platform, and wired up to the right instance when the continuation is - loaded off disk again. +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. -The buyer is supposed to send us a transaction with all the right inputs/outputs/commands in response to the opening -message, with their cash put into the transaction and their signature on it authorising the movement of the cash. +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. -You get back a simple wrapper class, ``UntrustworthyData``, which is just a marker class that reminds -us that the data came from a potentially malicious external source and may have been tampered with or be unexpected in -other ways. It doesn't add any functionality, but acts as a reminder to "scrub" the data before use. +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. If we choose the seller side as +the initiator then the buyer side would need to register their flow, perhaps with something like: -Our "scrubbing" has three parts: +.. container:: codeset -1. Check that the expected signatures are present and correct. At this point we expect our own signature to be missing, - because of course we didn't sign it yet, and also the signature of the notary because that must always come last. -2. We resolve the transaction, which we will cover below. -3. We verify that the transaction is paying us the demanded price. + .. sourcecode:: kotlin -Exception handling ------------------- + class TwoPartyTradeFlowPlugin : CordaPluginRegistry() { + override val servicePlugins = listOf(Function(TwoPartyTradeFlowService::Service)) + } -Flows can throw exceptions to prematurely terminate their execution. The flow framework gives special treatment to -``FlowException`` and its subtypes. These exceptions are treated as error responses of the flow and are propagated -to all counterparties it is communicating with. The receiving flows will throw the same exception the next time they do -a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` (details below) -then the exception can be caught there enabling re-invocation of the sub-flow. + object TwoPartyTradeFlowService { + class Service(services: PluginServiceHub) { + init { + services.registerServiceFlow(TwoPartyTradeFlow.Seller::class.java) { + TwoPartyTradeFlow.Buyer( + it, + notary = services.networkMapCache.notaryNodes[0].notaryIdentity, + acceptablePrice = TODO(), + typeToBuy = TODO()) + } + } + } + } -If the exception thrown by the erroring flow is not a ``FlowException`` it will still terminate but will not propagate to -the other counterparties. Instead they will be informed the flow has terminated and will themselves be terminated with a -generic exception. +This is telling the buyer node to fire up an instance of ``TwoPartyTradeFlow.Buyer`` (the code in the lambda) when +they receive a message from the initiating seller side of the flow (``TwoPartyTradeFlow.Seller::class.java``). -.. note:: A future version will extend this to give the node administrator more control on what to do with such erroring - flows. +.. _subflows: -Throwing a ``FlowException`` enables a flow to reject a piece of data it has received back to the sender. This is typically -done in the ``unwrap`` method of the received ``UntrustworthyData``. In the above example the seller checks the price -and throws ``FlowException`` if it's invalid. It's then up to the buyer to either try again with a better price or give up. - -Sub-flows and finalisation --------------------------- +Sub-flows +--------- Flows can be composed via nesting. Invoking a sub-flow looks similar to an ordinary function call: @@ -345,122 +367,124 @@ Flows can be composed via nesting. Invoking a sub-flow looks similar to an ordin subFlow(new FinalityFlow(unnotarisedTransaction)) } -In this code snippet we are using the ``FinalityFlow`` to finish off the transaction. It will: +Let's take a look at the three subflows we invoke in this flow. + +FinalityFlow +^^^^^^^^^^^^ +On the buyer side, we use ``FinalityFlow`` to finalise the transaction. It will: * Send the transaction to the chosen notary and, if necessary, satisfy the notary that the transaction is valid. * Record the transaction in the local vault, if it is relevant (i.e. involves the owner of the node). * Send the fully signed transaction to the other participants for recording as well. -We simply create the flow object via its constructor, and then pass it to the ``subFlow`` method which -returns the result of the flow's execution directly. Behind the scenes all this is doing is wiring up progress -tracking (discussed more below) and then running the objects ``call`` method. Because the sub-flow might suspend, -we must mark the method that invokes it as suspendable. - -Going back to the previous code snippet, we use a sub-flow called ``ResolveTransactionsFlow``. This is -responsible for downloading and checking all the dependencies of a transaction, which in Corda are always retrievable -from the party that sent you a transaction that uses them. This flow returns a list of ``LedgerTransaction`` -objects, but we don't need them here so we just ignore the return value. - -.. note:: Transaction dependency resolution assumes that the peer you got the transaction from has all of the - dependencies itself. It must do, otherwise it could not have convinced itself that the dependencies were themselves - valid. It's important to realise that requesting only the transactions we require is a privacy leak, because if - we don't download a transaction from the peer, they know we must have already seen it before. Fixing this privacy - leak will come later. - -After the dependencies, we check the proposed trading transaction for validity by running the contracts for that as -well (but having handled the fact that some signatures are missing ourselves). - .. warning:: If the seller stops before sending the finalised transaction to the buyer, the seller is left with a valid transaction but the buyer isn't, so they can't spend the asset they just purchased! This sort of thing is not always a risk (as the seller may not gain anything from that sort of behaviour except a lawsuit), but if it is, a future version of the platform will allow you to ask the notary to send you the transaction as well, in case your counterparty does not. This is not the default because it reveals more private info to the notary. -Implementing the buyer ----------------------- +We simply create the flow object via its constructor, and then pass it to the ``subFlow`` method which +returns the result of the flow's execution directly. Behind the scenes all this is doing is wiring up progress +tracking (discussed more below) and then running the object's ``call`` method. Because the sub-flow might suspend, +we must mark the method that invokes it as suspendable. -OK, let's do the same for the buyer side: +Within FinalityFlow, we use a further sub-flow called ``ResolveTransactionsFlow``. This is responsible for downloading +and checking all the dependencies of a transaction, which in Corda are always retrievable from the party that sent you a +transaction that uses them. This flow returns a list of ``LedgerTransaction`` objects. + +.. note:: Transaction dependency resolution assumes that the peer you got the transaction from has all of the + dependencies itself. It must do, otherwise it could not have convinced itself that the dependencies were themselves + valid. It's important to realise that requesting only the transactions we require is a privacy leak, because if + we don't download a transaction from the peer, they know we must have already seen it before. Fixing this privacy + leak will come later. + +CollectSignaturesFlow/SignTransactionFlow +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +We also invoke two other subflows: + +* ``CollectSignaturesFlow``, on the buyer side +* ``SignTransactionFlow``, on the seller side + +These flows communicate to gather all the required signatures for the proposed transaction. ``CollectSignaturesFlow`` +will: + +* Verify any signatures collected on the transaction so far +* Verify the transaction itself +* Send the transaction to the remaining required signers and receive back their signatures +* Verify the collected signatures + +``SignTransactionFlow`` responds by: + +* Receiving the partially-signed transaction off the wire +* Verifying the existing signatures +* Resolving the transaction's dependencies +* Verifying the transaction itself +* Running any custom validation logic +* Sending their signature back to the buyer +* Waiting for the transaction to be recorded in their vault + +We cannot instantiate ``SignTransactionFlow`` itself, as it's an abstract class. Instead, we need to subclass it and +override ``checkTransaction()`` to add our own custom validation logic: .. container:: codeset .. literalinclude:: ../../finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - :dedent: 4 + :language: kotlin + :start-after: DOCSTART 5 + :end-before: DOCEND 5 + :dedent: 12 -This code is longer but no more complicated. Here are some things to pay attention to: +In this case, our custom validation logic ensures that the amount of cash outputs in the transaction equals the +price of the asset. -1. We do some sanity checking on the received message to ensure we're being offered what we expected to be offered. -2. We create a cash spend using ``VaultService.generateSpend``. You can read the vault documentation to learn more about this. -3. We access the *service hub* when we need it to access things that are transient and may change or be recreated - whilst a flow is suspended, things like the wallet or the network map. -4. We send the unfinished, invalid transaction to the seller so they can sign it and finalise it. -5. Finally, we wait for the finished transaction to arrive in our local storage and vault. +Persisting flows +---------------- -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. +If you look at the code for ``FinalityFlow``, ``CollectSignaturesFlow`` and ``SignTransactionFlow``, you'll see calls +to both ``receive`` and ``sendAndReceive``. Once either of these methods is called, the ``call`` method will be +suspended into a continuation and saved to persistent storage. If the node crashes or is restarted, the flow will +effectively continue as if nothing had happened. Your code may remain blocked inside such a call for seconds, +minutes, hours or even days in the case of a flow that needs human interaction! -Flow sessions -------------- +.. note:: There are a couple of rules you need to bear in mind when writing a class that will be used as a continuation. + The first is that anything on the stack when the function is suspended will be stored into the heap and kept alive by + the garbage collector. So try to avoid keeping enormous data structures alive unless you really have to. You can + always use private methods to keep the stack uncluttered with temporary variables, or to avoid objects that + Kryo is not able to serialise correctly. -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. + The second is that as well as being kept on the heap, objects reachable from the stack will be serialised. The state + of the function call may be resurrected much later! Kryo doesn't require objects be marked as serialisable, but even so, + doing things like creating threads from inside these calls would be a bad idea. They should only contain business + logic and only do I/O via the methods exposed by the flow framework. -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. + It's OK to keep references around to many large internal node services though: these will be serialised using a + special token that's recognised by the platform, and wired up to the right instance when the continuation is + loaded off disk again. -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``. +``receive`` and ``sendAndReceive`` return a simple wrapper class, ``UntrustworthyData``, which is +just a marker class that reminds us that the data came from a potentially malicious external source and may have been +tampered with or be unexpected in other ways. It doesn't add any functionality, but acts as a reminder to "scrub" +the data before use. -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: +Exception handling +------------------ -.. container:: codeset +Flows can throw exceptions to prematurely terminate their execution. The flow framework gives special treatment to +``FlowException`` and its subtypes. These exceptions are treated as error responses of the flow and are propagated +to all counterparties it is communicating with. The receiving flows will throw the same exception the next time they do +a ``receive`` or ``sendAndReceive`` and thus end the flow session. If the receiver was invoked via ``subFlow`` (details below) +then the exception can be caught there enabling re-invocation of the sub-flow. - .. sourcecode:: kotlin +If the exception thrown by the erroring flow is not a ``FlowException`` it will still terminate but will not propagate to +the other counterparties. Instead they will be informed the flow has terminated and will themselves be terminated with a +generic exception. - @InitiatingFlow - class SellerStarter(val otherParty: Party, val assetToSell: StateAndRef, val price: Amount) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction { - val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] - val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey - return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey)) - } - } +.. note:: A future version will extend this to give the node administrator more control on what to do with such erroring +flows. -The buyer side would then need to register their flow, perhaps with something like: - -.. container:: codeset - - .. sourcecode:: kotlin - - val services: PluginServiceHub = TODO() - services.registerServiceFlow(SellerStarter::class.java) { otherParty -> - val notary = services.networkMapCache.notaryNodes[0] - val acceptablePrice = TODO() - val typeToBuy = TODO() - Buyer(otherParty, notary, acceptablePrice, typeToBuy) - } - -This is telling the buyer node to fire up an instance of ``Buyer`` (the code in the lambda) when the initiating flow -is a seller (``SellerStarter::class.java``). +Throwing a ``FlowException`` enables a flow to reject a piece of data it has received back to the sender. This is typically +done in the ``unwrap`` method of the received ``UntrustworthyData``. In the above example the seller checks the price +and throws ``FlowException`` if it's invalid. It's then up to the buyer to either try again with a better price or give up. .. _progress-tracking: @@ -489,22 +513,28 @@ A flow might declare some steps with code inside the flow class like this: .. sourcecode:: java private final ProgressTracker progressTracker = new ProgressTracker( - CONSTRUCTING_OFFER, - SENDING_OFFER_AND_RECEIVING_PARTIAL_TRANSACTION, - VERIFYING + RECEIVING, + VERIFYING, + SIGNING, + COLLECTING_SIGNATURES, + RECORDING ); - private static final ProgressTracker.Step CONSTRUCTING_OFFER = new ProgressTracker.Step( - "Constructing proposed purchase order."); - private static final ProgressTracker.Step SENDING_OFFER_AND_RECEIVING_PARTIAL_TRANSACTION = new ProgressTracker.Step( - "Sending purchase order to seller for review, and receiving partially signed transaction from seller in return."); + private static final ProgressTracker.Step RECEIVING = new ProgressTracker.Step( + "Waiting for seller trading info"); private static final ProgressTracker.Step VERIFYING = new ProgressTracker.Step( - "Verifying signatures and contract constraints."); + "Verifying seller assets"); + private static final ProgressTracker.Step SIGNING = new ProgressTracker.Step( + "Generating and signing transaction proposal"); + private static final ProgressTracker.Step COLLECTING_SIGNATURES = new ProgressTracker.Step( + "Collecting signatures from other parties"); + private static final ProgressTracker.Step RECORDING = new ProgressTracker.Step( + "Recording completed transaction"); -Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep`` -you can make a step that can update its label on the fly. That's useful for steps that want to expose non-structured -progress information like the current file being downloaded. By defining your own step types, you can export progress -in a way that's both human readable and machine readable. +Each step exposes a label. By default labels are fixed, but by subclassing ``RelabelableStep`` you can make a step +that can update its label on the fly. That's useful for steps that want to expose non-structured progress information +like the current file being downloaded. By defining your own step types, you can export progress in a way that's both +human readable and machine readable. Progress trackers are hierarchical. Each step can be the parent for another tracker. By altering the ``ProgressTracker.childrenFor`` map, a tree of steps can be created. It's allowed to alter the hierarchy @@ -523,9 +553,9 @@ steps by overriding the ``Step`` class like this: .. sourcecode:: java - private static final ProgressTracker.Step COMMITTING = new ProgressTracker.Step("Committing to the ledger.") { + private static final ProgressTracker.Step VERIFYING_AND_SIGNING = new ProgressTracker.Step("Verifying and signing transaction proposal") { @Nullable @Override public ProgressTracker childProgressTracker() { - return FinalityFlow.Companion.tracker(); + return SignTransactionFlow.Companion.tracker(); } }; @@ -583,4 +613,4 @@ the features we have planned: * Being able to interact with people, either via some sort of external ticketing system, or email, or a custom UI. For example to implement human transaction authorisations. * A standard library of flows that can be easily sub-classed by local developers in order to integrate internal - reporting logic, or anything else that might be required as part of a communications lifecycle. + reporting logic, or anything else that might be required as part of a communications lifecycle. \ No newline at end of file diff --git a/docs/source/flow-testing.rst b/docs/source/flow-testing.rst index 73b062998d..acf3e86be2 100644 --- a/docs/source/flow-testing.rst +++ b/docs/source/flow-testing.rst @@ -20,24 +20,24 @@ with this basic skeleton: .. sourcecode:: kotlin class ResolveTransactionsFlowTest { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var a: MockNetwork.MockNode lateinit var b: MockNetwork.MockNode lateinit var notary: Party @Before fun setup() { - net = MockNetwork() - val nodes = net.createSomeNodes() + mockNet = MockNetwork() + val nodes = mockNet.createSomeNodes() a = nodes.partyNodes[0] b = nodes.partyNodes[1] notary = nodes.notaryNode.info.notaryIdentity - net.runNetwork() + mockNet.runNetwork() } @After fun tearDown() { - net.stopNodes() + mockNet.stopNodes() } } @@ -56,7 +56,7 @@ We'll take a look at the ``makeTransactions`` function in a moment. For now, it' but not node B. The test logic is simple enough: we create the flow, giving it node A's identity as the target to talk to. -Then we start it on node B and use the ``net.runNetwork()`` method to bounce messages around until things have +Then we start it on node B and use the ``mockNet.runNetwork()`` method to bounce messages around until things have settled (i.e. there are no more messages waiting to be delivered). All this is done using an in memory message routing implementation that is fast to initialise and use. Finally, we obtain the result of the flow and do some tests on it. We also check the contents of node B's database to see that the flow had the intended effect @@ -80,4 +80,9 @@ valid) inside a ``database.transaction``. All node flows run within a database but any time we need to use the database directly from a unit test, you need to provide a database transaction as shown here. -And that's it: you can explore the documentation for the `MockNetwork API `_ here. +With regards to initiated flows (see :doc:`flow-state-machines` for information on initiated and initiating flows), the +full node automatically registers them by scanning the CorDapp jars. In a unit test environment this is not possible so +``MockNode`` has the ``registerInitiatedFlow`` method to manually register an initiated flow. + +And that's it: you can explore the documentation for the `MockNetwork API `_ +here. diff --git a/docs/source/further-notes-on-kotlin.rst b/docs/source/further-notes-on-kotlin.rst deleted file mode 100644 index 64992d82ac..0000000000 --- a/docs/source/further-notes-on-kotlin.rst +++ /dev/null @@ -1,14 +0,0 @@ -Further notes on Kotlin ------------------------ - -Corda is written in a language called `Kotlin `_. Kotlin is a language that targets the JVM -and can be thought of as a simpler Scala, with much better Java interop. It is developed by and has commercial support -from JetBrains, the makers of the IntelliJ IDE and other popular developer tools. - -Kotlin is a relatively new language and is extremely easy to learn. It is designed as a better Java for industrial -use and, as such, the syntax was carefully designed to be readable even to people who don't know the language, after only -a few minutes of introduction. Additionally, at R3, we find that all of our developers are up to productive writing speed -in Kotlin within their first week. - -Due to the seamless Java interop the use of Kotlin to extend the platform is *not* required and the tutorial shows how -to write contracts in both Kotlin and Java. You can `read more about why Kotlin is a potentially strong successor to Java here `_. diff --git a/docs/source/getting-set-up.rst b/docs/source/getting-set-up.rst index 690b779374..ac221d2217 100644 --- a/docs/source/getting-set-up.rst +++ b/docs/source/getting-set-up.rst @@ -3,54 +3,49 @@ Getting set up Software requirements --------------------- +Corda uses industry-standard tools: -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. +* **Oracle JDK 8 JVM** - supported version **8u131** +* **IntelliJ IDEA** - supported versions **2017.1**, **2017.2** and **2017.3** +* **Gradle** - supported version **3.4** +* **Kotlin** - supported version **1.1.2** +* **Git** -JVM -~~~ +You do not need to install Gradle or Kotlin. A standalone Gradle wrapper is provided, and it will download the correct +version of Kotlin. -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 note: -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. +* Corda runs in a JVM. JVM implementations other than Oracle JDK 8 are not actively supported. However, if you do + choose to use OpenJDK, you will also need to install OpenJFX -If you do choose to use OpenJDK instead of Oracle's JDK, you will also need to install OpenJFX. +* Applications on Corda (CorDapps) can be written in any language targeting the JVM. However, Corda itself and most of + the samples are written in Kotlin. Kotlin is an + `official Android language `_, and you can read more about why + Kotlin is a strong successor to Java + `here `_. If you're + unfamiliar with Kotlin, there is an official + `getting started guide `_, and a series of + `Kotlin Koans `_. -Kotlin -~~~~~~ +* IntelliJ IDEA is recommended due to the strength of its Kotlin integration. -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. - -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. - -Gradle -~~~~~~ - -We use Gradle as the build tool for Corda. However, you do not need to install Gradle itself, as a wrapper is provided. +Following these software recommendations 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. 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 `_. +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:`troubleshooting` 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" @@ -59,19 +54,19 @@ Java 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`` @@ -79,14 +74,14 @@ Download a sample project 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 @@ -100,10 +95,10 @@ Run from IntelliJ 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 @@ -111,12 +106,12 @@ Java 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`` @@ -124,14 +119,14 @@ Download a sample project 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". @@ -158,7 +153,8 @@ 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 unstable ``master`` branch. You should check out the latest milestone release instead by running ``git checkout release-M11.1``. +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-M12``. Next steps ---------- @@ -168,5 +164,5 @@ The best way to check that everything is working fine is by :doc:`running-the-de Once you have these demos running, you may be interested in writing your own CorDapps, in which case you should refer to :doc:`tutorial-cordapp`. -If you encounter any issues, please see the :doc:`getting-set-up-fault-finding` page, or get in touch with us on the +If you encounter any issues, please see the :doc:`troubleshooting` page, or get in touch with us on the `forums `_ or via `slack `_. diff --git a/docs/source/hello-world-contract.rst b/docs/source/hello-world-contract.rst new file mode 100644 index 0000000000..3c0ab482f4 --- /dev/null +++ b/docs/source/hello-world-contract.rst @@ -0,0 +1,738 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Writing the contract +==================== + +In Corda, the ledger is updated via transactions. Each transaction is a proposal to mark zero or more existing +states as historic (the inputs), while creating zero or more new states (the outputs). + +It's easy to imagine that most CorDapps will want to impose some constraints on how their states evolve over time: + +* A cash CorDapp would not want to allow users to create transactions that generate money out of thin air (at least + without the involvement of a central bank or commercial bank) +* A loan CorDapp might not want to allow the creation of negative-valued loans +* An asset-trading CorDapp would not want to allow users to finalise a trade without the agreement of their counterparty + +In Corda, we impose constraints on what transactions are allowed using contracts. These contracts are very different +to the smart contracts of other distributed ledger platforms. They do not represent the current state of the ledger. +Instead, like a real-world contract, they simply impose rules on what kinds of agreements are allowed. + +Every state is associated with a contract. A transaction is invalid if it does not satisfy the contract of every +input and output state in the transaction. + +The Contract interface +---------------------- +Just as every Corda state must implement the ``ContractState`` interface, every contract must implement the +``Contract`` interface: + +.. container:: codeset + + .. code-block:: kotlin + + interface Contract { + // Implements the contract constraints in code. + @Throws(IllegalArgumentException::class) + fun verify(tx: TransactionForContract) + + // Expresses the contract constraints as legal prose. + val legalContractReference: SecureHash + } + +A few more Kotlinisms here: + +* ``fun`` declares a function +* The syntax ``fun funName(arg1Name: arg1Type): returnType`` declares that ``funName`` takes an argument of type + ``arg1Type`` and returns a value of type ``returnType`` + +We can see that ``Contract`` expresses its constraints in two ways: + +* In legal prose, through a hash referencing a legal contract that expresses the contract's constraints in legal prose +* In code, through a ``verify`` function that takes a transaction as input, and: + + * Throws an ``IllegalArgumentException`` if it rejects the transaction proposal + * Returns silently if it accepts the transaction proposal + +Controlling IOU evolution +------------------------- +What would a good contract for an ``IOUState`` look like? There is no right or wrong answer - it depends on how you +want your CorDapp to behave. + +For our CorDapp, let's impose the constraint that we only want to allow the creation of IOUs. We don't want nodes to +transfer them or redeem them for cash. One way to enforce this behaviour would be by imposing the following constraints: + +* A transaction involving IOUs must consume zero inputs, and create one output of type ``IOUState`` +* The transaction should also include a ``Create`` command, indicating the transaction's intent (more on commands + shortly) +* For the transactions's output IOU state: + + * Its value must be non-negative + * Its sender and its recipient cannot be the same entity + * All the participants (i.e. both the sender and the recipient) must sign the transaction + +We can picture this transaction as follows: + + .. image:: resources/tutorial-transaction.png +:scale: 15% + :align: center + +Let's write a contract that enforces these constraints. We'll do this by modifying either ``TemplateContract.java`` or +``TemplateContract.kt`` and updating ``TemplateContract`` to define an ``IOUContract``. + +Defining IOUContract +-------------------- + +The Create command +^^^^^^^^^^^^^^^^^^ +The first thing our contract needs is a *command*. Commands serve two purposes: + +* They indicate the transaction's intent, allowing us to perform different verification given the situation + + * For example, a transaction proposing the creation of an IOU could have to satisfy different constraints to one + redeeming an IOU + +* They allow us to define the required signers for the transaction + + * For example, IOU creation might require signatures from both the sender and the recipient, whereas the transfer + of an IOU might only require a signature from the IOUs current holder + +Let's update the definition of ``TemplateContract`` (in ``TemplateContract.java`` or ``TemplateContract.kt``) to +define an ``IOUContract`` with a ``Create`` command: + +.. container:: codeset + + .. code-block:: kotlin + + package com.template + + import net.corda.core.contracts.* + import net.corda.core.crypto.SecureHash + import net.corda.core.crypto.SecureHash.Companion.sha256 + + open class IOUContract : Contract { + // Currently, verify() does no checking at all! + override fun verify(tx: TransactionForContract) {} + + // Our Create command. + class Create : CommandData + + // The legal contract reference - we'll leave this as a dummy hash for now. + override val legalContractReference = SecureHash.sha256("Prose contract.") + } + + .. code-block:: java + + package com.template; + + import net.corda.core.contracts.CommandData; + import net.corda.core.contracts.Contract; + import net.corda.core.crypto.SecureHash; + + public class IOUContract implements Contract { + @Override + // Currently, verify() does no checking at all! + public void verify(TransactionForContract tx) {} + + // Our Create command. + public static class Create implements CommandData {} + + // The legal contract reference - we'll leave this as a dummy hash for now. + private final SecureHash legalContractReference = SecureHash.sha256("Prose contract."); + @Override public final SecureHash getLegalContractReference() { return legalContractReference; } + } + +Aside from renaming ``TemplateContract`` to ``IOUContract``, we've also implemented the ``Create`` command. All +commands must implement the ``CommandData`` interface. + +The ``CommandData`` interface is a simple marker interface for commands. In fact, its declaration is only two words +long (in Kotlin, interfaces do not require a body): + +.. container:: codeset + + .. code-block:: kotlin + + interface CommandData + +The verify logic +^^^^^^^^^^^^^^^^ +We now need to define the actual contract constraints. For our IOU CorDapp, we won't concern ourselves with writing +valid legal prose to enforce the IOU agreement in court. Instead, we'll focus on implementing ``verify``. + +Remember that our goal in writing the ``verify`` function is to write a function that, given a transaction: + +* Throws an ``IllegalArgumentException`` if the transaction is considered invalid +* Does **not** throw an exception if the transaction is considered valid + +In deciding whether the transaction is valid, the ``verify`` function only has access to the contents of the +transaction: + +* ``tx.inputs``, which lists the inputs +* ``tx.outputs``, which lists the outputs +* ``tx.commands``, which lists the commands and their associated signers + +Although we won't use them here, the ``verify`` function also has access to the transaction's attachments, +time-windows, notary and hash. + +Based on the constraints enumerated above, we'll write a ``verify`` function that rejects a transaction if any of the +following are true: + +* The transaction doesn't include a ``Create`` command +* The transaction has inputs +* The transaction doesn't have exactly one output +* The IOU itself is invalid +* The transaction doesn't require signatures from both the sender and the recipient + +Let's work through these constraints one-by-one. + +Command constraints +~~~~~~~~~~~~~~~~~~~ +To test for the presence of the ``Create`` command, we can use Corda's ``requireSingleCommand`` function: + +.. container:: codeset + + .. code-block:: kotlin + + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + } + + .. code-block:: java + + // Additional imports. + import net.corda.core.contracts.AuthenticatedObject; + import net.corda.core.contracts.TransactionForContract; + import static net.corda.core.contracts.ContractsDSL.requireSingleCommand; + + ... + + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + } + +Here, ``requireSingleCommand`` performing a dual purpose: + +* It's asserting that there is exactly one ``Create`` command in the transaction +* It's extracting the command and returning it + +If the ``Create`` command isn't present, or if the transaction has multiple ``Create`` commands, contract +verification will fail. + +Transaction constraints +~~~~~~~~~~~~~~~~~~~~~~~ +We also wanted our transaction to have no inputs and only a single output. One way to impose this constraint is as +follows: + +.. container:: codeset + + .. code-block:: kotlin + + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "Only one output state should be created." using (tx.outputs.size == 1) + } + } + + .. code-block:: java + + // Additional import. + import static net.corda.core.contracts.ContractsDSL.requireThat; + + ... + + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("Only one output state should be created.", tx.getOutputs().size() == 1); + + return null; + }); + } + +Note the use of Corda's built-in ``requireThat`` function. ``requireThat`` provides a terse way to write the following: + +* If the condition on the right-hand side doesn't evaluate to true... +* ...throw an ``IllegalArgumentException`` with the message on the left-hand side + +As before, the act of throwing this exception would cause transaction verification to fail. + +IOU constraints +~~~~~~~~~~~~~~~ +We want to impose two constraints on the ``IOUState`` itself: + +* Its value must be non-negative +* Its sender and its recipient cannot be the same entity + +We can impose these constraints in the same ``requireThat`` block as before: + +.. container:: codeset + + .. code-block:: kotlin + + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "Only one output state should be created." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputs.single() as IOUState + "The IOU's value must be non-negative." using (out.value > 0) + "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) + } + } + + .. code-block:: java + + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("Only one output state should be created.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = (IOUState) tx.getOutputs().get(0); + check.using("The IOU's value must be non-negative.",out.getValue() > 0); + check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); + + return null; + }); + } + +You can see that we're not restricted to only writing constraints in the ``requireThat`` block. We can also write +other statements - in this case, we're extracting the transaction's single ``IOUState`` and assigning it to a variable. + +Signer constraints +~~~~~~~~~~~~~~~~~~ +Our final constraint is that the required signers on the transaction are the sender and the recipient only. A +transaction's required signers is equal to the union of all the signers listed on the commands. We can therefore +extract the signers from the ``Create`` command we retrieved earlier. + +.. container:: codeset + + .. code-block:: kotlin + + override fun verify(tx: TransactionForContract) { + val command = tx.commands.requireSingleCommand() + + requireThat { + // Constraints on the shape of the transaction. + "No inputs should be consumed when issuing an IOU." using (tx.inputs.isEmpty()) + "Only one output state should be created." using (tx.outputs.size == 1) + + // IOU-specific constraints. + val out = tx.outputs.single() as IOUState + "The IOU's value must be non-negative." using (out.value > 0) + "The sender and the recipient cannot be the same entity." using (out.sender != out.recipient) + + // Constraints on the signers. + "All of the participants must be signers." using (command.signers.toSet() == out.participants.map { it.owningKey }.toSet()) + } + } + + .. code-block:: java + + // Additional imports. + import com.google.common.collect.ImmutableList; + import java.security.PublicKey; + import java.util.List; + + ... + + @Override + public void verify(TransactionForContract tx) { + final AuthenticatedObject command = requireSingleCommand(tx.getCommands(), Create.class); + + requireThat(check -> { + // Constraints on the shape of the transaction. + check.using("No inputs should be consumed when issuing an IOU.", tx.getInputs().isEmpty()); + check.using("Only one output state should be created.", tx.getOutputs().size() == 1); + + // IOU-specific constraints. + final IOUState out = (IOUState) tx.getOutputs().get(0); + final Party sender = out.getSender(); + final Party recipient = out.getRecipient(); + check.using("The IOU's value must be non-negative.",out.getValue() > 0); + check.using("The sender and the recipient cannot be the same entity.", out.getSender() != out.getRecipient()); + + // Constraints on the signers. + final Set requiredSigners = Sets.newHashSet(sender.getOwningKey(), recipient.getOwningKey()); + final Set signerSet = Sets.newHashSet(command.getSigners()); + check.using("All of the participants must be signers.", (signerSet.equals(requiredSigners))); + + return null; + }); + } + +Checkpoint +---------- +We've now defined the full contract logic of our ``IOUContract``. This contract means that transactions involving +``IOUState`` states will have to fulfill strict constraints to become valid ledger updates. + +Before we move on, let's go back and modify ``IOUState`` to point to the new ``IOUContract``: + +.. container:: codeset + + .. code-block:: kotlin + + class IOUState(val value: Int, + val sender: Party, + val recipient: Party) : ContractState { + override val contract: IOUContract = IOUContract() + + override val participants get() = listOf(sender, recipient) + } + + .. code-block:: java + + public class IOUState implements ContractState { + private final Integer value; + private final Party sender; + private final Party recipient; + private final IOUContract contract = new IOUContract(); + + public IOUState(Integer value, Party sender, Party recipient) { + this.value = value; + this.sender = sender; + this.recipient = recipient; + } + + public Integer getValue() { + return value; + } + + public Party getSender() { + return sender; + } + + public Party getRecipient() { + return recipient; + } + + @Override + public IOUContract getContract() { + return contract; + } + + @Override + public List getParticipants() { + return ImmutableList.of(sender, recipient); + } + } + +Transaction tests +----------------- +How can we ensure that we've defined our contract constraints correctly? + +One option would be to deploy the CorDapp onto a set of nodes, and test it manually. However, this is a relatively +slow process, and would take on the order of minutes to test each change. + +Instead, we can test our contract logic using Corda's ``ledgerDSL`` transaction-testing framework. This will allow us +to test our contract without the overhead of spinning up a set of nodes. + +Open either ``test/kotlin/com/template/contract/ContractTests.kt`` or +``test/java/com/template/contract/ContractTests.java``, and add the following as our first test: + +.. container:: codeset + + .. code-block:: kotlin + + package com.template + + import net.corda.testing.* + import org.junit.Test + + class IOUTransactionTests { + @Test + fun `transaction must include Create command`() { + ledger { + transaction { + output { IOUState(1, MINI_CORP, MEGA_CORP) } + fails() + command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } + verifies() + } + } + } + } + + .. code-block:: java + + package com.template; + + import net.corda.core.identity.Party; + import org.junit.Test; + import java.security.PublicKey; + import static net.corda.testing.CoreTestUtils.*; + + public class IOUTransactionTests { + static private final Party miniCorp = getMINI_CORP(); + static private final Party megaCorp = getMEGA_CORP(); + static private final PublicKey[] keys = new PublicKey[2]; + + { + keys[0] = getMEGA_CORP_PUBKEY(); + keys[1] = getMINI_CORP_PUBKEY(); + } + + @Test + public void transactionMustIncludeCreateCommand() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + txDSL.fails(); + txDSL.command(keys, IOUContract.Create::new); + txDSL.verifies(); + return null; + }); + return null; + }); + } + } + +This test uses Corda's built-in ``ledgerDSL`` to: + +* Create a fake transaction +* Add inputs, outputs, commands, etc. (using the DSL's ``output``, ``input`` and ``command`` methods) +* At any point, asserting that the transaction built so far is either contractually valid (by calling ``verifies``) or + contractually invalid (by calling ``fails``) + +In this instance: + +* We initially create a transaction with an output but no command +* We assert that this transaction is invalid (since the ``Create`` command is missing) +* We then add the ``Create`` command +* We assert that transaction is now valid + +Here is the full set of tests we'll be using to test the ``IOUContract``: + +.. container:: codeset + + .. code-block:: kotlin + + class IOUTransactionTests { + @Test + fun `transaction must include Create command`() { + ledger { + transaction { + output { IOUState(1, MINI_CORP, MEGA_CORP) } + fails() + command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } + verifies() + } + } + } + + @Test + fun `transaction must have no inputs`() { + ledger { + transaction { + input { IOUState(1, MINI_CORP, MEGA_CORP) } + output { IOUState(1, MINI_CORP, MEGA_CORP) } + command(MEGA_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("No inputs should be consumed when issuing an IOU.") + } + } + } + + @Test + fun `transaction must have one output`() { + ledger { + transaction { + output { IOUState(1, MINI_CORP, MEGA_CORP) } + output { IOUState(1, MINI_CORP, MEGA_CORP) } + command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("Only one output state should be created.") + } + } + } + + @Test + fun `sender must sign transaction`() { + ledger { + transaction { + output { IOUState(1, MINI_CORP, MEGA_CORP) } + command(MINI_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("All of the participants must be signers.") + } + } + } + + @Test + fun `recipient must sign transaction`() { + ledger { + transaction { + output { IOUState(1, MINI_CORP, MEGA_CORP) } + command(MEGA_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("All of the participants must be signers.") + } + } + } + + @Test + fun `sender is not recipient`() { + ledger { + transaction { + output { IOUState(1, MEGA_CORP, MEGA_CORP) } + command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("The sender and the recipient cannot be the same entity.") + } + } + } + + @Test + fun `cannot create negative-value IOUs`() { + ledger { + transaction { + output { IOUState(-1, MINI_CORP, MEGA_CORP) } + command(MEGA_CORP_PUBKEY, MINI_CORP_PUBKEY) { IOUContract.Create() } + `fails with`("The IOU's value must be non-negative.") + } + } + } + } + + .. code-block:: java + + public class IOUTransactionTests { + static private final Party miniCorp = getMINI_CORP(); + static private final Party megaCorp = getMEGA_CORP(); + static private final PublicKey[] keys = new PublicKey[2]; + + { + keys[0] = getMEGA_CORP_PUBKEY(); + keys[1] = getMINI_CORP_PUBKEY(); + } + + @Test + public void transactionMustIncludeCreateCommand() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + txDSL.fails(); + txDSL.command(keys, IOUContract.Create::new); + txDSL.verifies(); + return null; + }); + return null; + }); + } + + @Test + public void transactionMustHaveNoInputs() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.input(new IOUState(1, miniCorp, megaCorp)); + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("No inputs should be consumed when issuing an IOU."); + return null; + }); + return null; + }); + } + + @Test + public void transactionMustHaveOneOutput() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("Only one output state should be created."); + return null; + }); + return null; + }); + } + + @Test + public void senderMustSignTransaction() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + PublicKey[] keys = new PublicKey[1]; + keys[0] = getMINI_CORP_PUBKEY(); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("All of the participants must be signers."); + return null; + }); + return null; + }); + } + + @Test + public void recipientMustSignTransaction() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, miniCorp, megaCorp)); + PublicKey[] keys = new PublicKey[1]; + keys[0] = getMEGA_CORP_PUBKEY(); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("All of the participants must be signers."); + return null; + }); + return null; + }); + } + + @Test + public void senderIsNotRecipient() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(1, megaCorp, megaCorp)); + PublicKey[] keys = new PublicKey[1]; + keys[0] = getMEGA_CORP_PUBKEY(); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("The sender and the recipient cannot be the same entity."); + return null; + }); + return null; + }); + } + + @Test + public void cannotCreateNegativeValueIOUs() { + ledger(ledgerDSL -> { + ledgerDSL.transaction(txDSL -> { + txDSL.output(new IOUState(-1, miniCorp, megaCorp)); + txDSL.command(keys, IOUContract.Create::new); + txDSL.failsWith("The IOU's value must be non-negative."); + return null; + }); + return null; + }); + } + } + +Copy these tests into the ContractTests file, and run them to ensure that the ``IOUState`` and ``IOUContract`` are +defined correctly. All the tests should pass. + +Progress so far +--------------- +We've now written an ``IOUContract`` constraining the evolution of each ``IOUState`` over time: + +* An ``IOUState`` can only be created, not transferred or redeemed +* Creating an ``IOUState`` requires an issuance transaction with no inputs, a single ``IOUState`` output, and a + ``Create`` command +* The ``IOUState`` created by the issuance transaction must have a non-negative value, and its sender and recipient + must be different entities. + +The final step in the creation of our CorDapp will be to write the ``IOUFlow`` that will allow nodes to orchestrate +the creation of a new ``IOUState`` on the ledger, while only sharing information on a need-to-know basis. \ No newline at end of file diff --git a/docs/source/hello-world-flow.rst b/docs/source/hello-world-flow.rst new file mode 100644 index 0000000000..8b71c7a494 --- /dev/null +++ b/docs/source/hello-world-flow.rst @@ -0,0 +1,1020 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Writing the flow +================ +A flow describes the sequence of steps for agreeing a specific ledger update. By installing new flows on our node, we +allow the node to handle new business processes. + +We'll have to define two flows to issue an ``IOUState`` onto the ledger: + +* One to be run by the node initiating the creation of the IOU +* One to be run by the node responding to an IOU creation request + +Let's start writing our flows. We'll do this by modifying either ``TemplateFlow.java`` or ``TemplateFlow.kt``. + +FlowLogic +--------- +Each flow is implemented as a ``FlowLogic`` subclass. You define the steps taken by the flow by overriding +``FlowLogic.call``. + +We will define two ``FlowLogic`` instances communicating as a pair. The first will be called ``Initiator``, and will +be run by the sender of the IOU. The other will be called ``Acceptor``, and will be run by the recipient. We group +them together using a class (in Java) or a singleton object (in Kotlin) to show that they are conceptually related. + +Overwrite the existing template code with the following: + +.. container:: codeset + + .. code-block:: kotlin + + package com.template + + import co.paralleluniverse.fibers.Suspendable + import net.corda.core.flows.FlowLogic + import net.corda.core.flows.InitiatedBy + import net.corda.core.flows.InitiatingFlow + import net.corda.core.flows.StartableByRPC + import net.corda.core.identity.Party + import net.corda.core.transactions.SignedTransaction + import net.corda.core.utilities.ProgressTracker + + object IOUFlow { + @InitiatingFlow + @StartableByRPC + class Initiator(val iouValue: Int, + val otherParty: Party): FlowLogic() { + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + override val progressTracker = ProgressTracker() + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + override fun call(): SignedTransaction { } + } + + @InitiatedBy(Initiator::class) + class Acceptor(val otherParty: Party) : FlowLogic() { + + @Suspendable + override fun call() { } + } + } + + .. code-block:: java + + package com.template; + + import co.paralleluniverse.fibers.Suspendable; + import net.corda.core.flows.*; + import net.corda.core.identity.Party; + import net.corda.core.transactions.SignedTransaction; + import net.corda.core.utilities.ProgressTracker; + + public class IOUFlow { + @InitiatingFlow + @StartableByRPC + public static class Initiator extends FlowLogic { + private final Integer iouValue; + private final Party otherParty; + + /** The progress tracker provides checkpoints indicating the progress of the flow to observers. */ + private final ProgressTracker progressTracker = new ProgressTracker(); + + public Initiator(Integer iouValue, Party otherParty) { + this.iouValue = iouValue; + this.otherParty = otherParty; + } + + /** The flow logic is encapsulated within the call() method. */ + @Suspendable + @Override + public SignedTransaction call() throws FlowException { } + } + + @InitiatedBy(Initiator.class) + public static class Acceptor extends FlowLogic { + + private final Party otherParty; + + public Acceptor(Party otherParty) { + this.otherParty = otherParty; + } + + @Suspendable + @Override + public Void call() throws FlowException { } + } + } + +We can see that we have two ``FlowLogic`` subclasses, each overriding ``FlowLogic.call``. There's a few things to note: + +* ``FlowLogic.call`` has a return type that matches the type parameter passed to ``FlowLogic`` - this is the return + type of running the flow +* The ``FlowLogic`` subclasses can have constructor parameters, which can be used as arguments to ``FlowLogic.call`` +* ``FlowLogic.call`` is annotated ``@Suspendable`` - this means that the flow will be check-pointed and serialised to + disk when it encounters a long-running operation, allowing your node to move on to running other flows. Forgetting + this annotation out will lead to some very weird error messages +* There are also a few more annotations, on the ``FlowLogic`` subclasses themselves: + + * ``@InitiatingFlow`` means that this flow can be started directly by the node + * ``StartableByRPC`` allows the node owner to start this flow via an RPC call + * ``@InitiatedBy(myClass: Class)`` means that this flow will only start in response to a message sent by another + node running the ``myClass`` flow + +Flow outline +------------ +Now that we've defined our ``FlowLogic`` subclasses, what are the steps we need to take to issue a new IOU onto +the ledger? + +On the initiator side, we need to: + + 1. Create a valid transaction proposal for the creation of a new IOU + 2. Verify the transaction + 3. Sign the transaction ourselves + 4. Gather the acceptor's signature + 5. Optionally get the transaction notarised, to: + + * Protect against double-spends for transactions with inputs + * Timestamp transactions that have a ``TimeWindow`` + + 6. Record the transaction in our vault + 7. Send the transaction to the acceptor so that they can record it too + +On the acceptor side, we need to: + + 1. Receive the partially-signed transaction from the initiator + 2. Verify its contents and signatures + 3. Append our signature and send it back to the initiator + 4. Wait to receive back the transaction from the initiator + 5. Record the transaction in our vault + +Subflows +^^^^^^^^ +Although our flow requirements look complex, we can delegate to existing flows to handle many of these tasks. A flow +that is invoked within the context of a larger flow to handle a repeatable task is called a *subflow*. + +In our initiator flow, we can automate step 4 by invoking ``SignTransactionFlow``, and we can automate steps 5, 6 and +7 using ``FinalityFlow``. Meanwhile, the *entirety* of the acceptor's flow can be automated using +``CollectSignaturesFlow``. + +All we need to do is write the steps to handle the initiator creating and signing the proposed transaction. + +Writing the initiator's flow +---------------------------- +Let's work through the steps of the initiator's flow one-by-one. + +Building the transaction +^^^^^^^^^^^^^^^^^^^^^^^^ +We'll approach building the transaction in three steps: + +* Creating a transaction builder +* Creating the transaction's components +* Adding the components to the builder + +TransactionBuilder +~~~~~~~~~~~~~~~~~~ +To start building the proposed transaction, we need a ``TransactionBuilder``. This is a mutable transaction class to +which we can add inputs, outputs, commands, and any other components the transaction needs. + +We create a ``TransactionBuilder`` in ``Initiator.call`` as follows: + +.. container:: codeset + + .. code-block:: kotlin + + // Additional import. + import net.corda.core.transactions.TransactionBuilder + + ... + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + } + + .. code-block:: java + + // Additional import. + import net.corda.core.transactions.TransactionBuilder; + + ... + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + } + +In the first line, we create a ``TransactionBuilder``. We will also want our transaction to have a notary, in order +to prevent double-spends. In the second line, we retrieve the identity of the notary who will be notarising our +transaction and add it to the builder. + +You can see that the notary's identity is being retrieved from the node's ``ServiceHub``. Whenever we need +information within a flow - whether it's about our own node, its contents, or the rest of the network - we use the +node's ``ServiceHub``. In particular, ``ServiceHub.networkMapCache`` provides information about the other nodes on the +network and the services that they offer. + +Transaction components +~~~~~~~~~~~~~~~~~~~~~~ +Now that we have our ``TransactionBuilder``, we need to create its components. Remember that we're trying to build +the following transaction: + + .. image:: resources/tutorial-transaction.png +:scale: 15% + :align: center + +So we'll need the following: + +* The output ``IOUState`` +* A ``Create`` command listing both the IOU's sender and recipient as signers + +We create these components as follows: + +.. container:: codeset + + .. code-block:: kotlin + + // Additional import. + import net.corda.core.contracts.Command + + ... + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + } + + .. code-block:: java + + // Additional imports. + import com.google.common.collect.ImmutableList; + import net.corda.core.contracts.Command; + import java.security.PublicKey; + import java.util.List; + + ... + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + } + +To build the state, we start by retrieving our own identity (again, we get this information from the ``ServiceHub``, +via ``ServiceHub.myInfo``). We then build the ``IOUState``, using our identity, the ``IOUContract``, and the IOU +value and counterparty from the ``FlowLogic``'s constructor parameters. + +We also create the command, which pairs the ``IOUContract.Create`` command with the public keys of ourselves and the +counterparty. If this command is included in the transaction, both ourselves and the counterparty will be required +signers. + +Adding the components +~~~~~~~~~~~~~~~~~~~~~ +Finally, we add the items to the transaction using the ``TransactionBuilder.withItems`` method: + +.. container:: codeset + + .. code-block:: kotlin + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand) + } + + .. code-block:: java + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand); + } + +``TransactionBuilder.withItems`` takes a `vararg` of: + +* `ContractState` objects, which are added to the builder as output states +* `StateRef` objects (references to the outputs of previous transactions), which are added to the builder as input + state references +* `Command` objects, which are added to the builder as commands + +It will modify the ``TransactionBuilder`` in-place to add these components to it. + +Verifying the transaction +^^^^^^^^^^^^^^^^^^^^^^^^^ +We've now built our proposed transaction. Before we sign it, we should check that it represents a valid ledger update +proposal by verifying the transaction, which will execute each of the transaction's contracts: + +.. container:: codeset + + .. code-block:: kotlin + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand) + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + } + + .. code-block:: java + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand); + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); + } + +To verify the transaction, we must: + +* Convert the builder into an immutable ``WireTransaction`` +* Convert the ``WireTransaction`` into a ``LedgerTransaction`` using the ``ServiceHub``. This step resolves the + transaction's input state references and attachment references into actual states and attachments (in case their + contents are needed to verify the transaction +* Call ``LedgerTransaction.verify`` to test whether the transaction is valid based on the contract of every input and + output state in the transaction + +If the verification fails, we have built an invalid transaction. Our flow will then end, throwing a +``TransactionVerificationException``. + +Signing the transaction +^^^^^^^^^^^^^^^^^^^^^^^ +Now that we are satisfied that our transaction proposal is valid, we sign it. Once the transaction is signed, +no-one will be able to modify the transaction without invalidating our signature. This effectively makes the +transaction immutable. + +.. container:: codeset + + .. code-block:: kotlin + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand) + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + + // Signing the transaction. + val partSignedTx = serviceHub.signInitialTransaction(txBuilder) + } + + .. code-block:: java + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand); + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); + + // Signing the transaction. + final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); + } + +The call to ``ServiceHub.signInitialTransaction`` returns a ``SignedTransaction`` - an object that pairs the +transaction itself with a list of signatures over that transaction. + +We can now safely send the builder to our counterparty. If the counterparty tries to modify the transaction, the +transaction's hash will change, our digital signature will no longer be valid, and the transaction will not be accepted +as a valid ledger update. + +Gathering counterparty signatures +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +The final step in order to create a valid transaction proposal is to collect the counterparty's signature. As +discussed, we can automate this process by invoking the built-in ``CollectSignaturesFlow``: + +.. container:: codeset + + .. code-block:: kotlin + + // Additional import. + import net.corda.flows.CollectSignaturesFlow + + ... + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand) + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + + // Signing the transaction. + val partSignedTx = serviceHub.signInitialTransaction(txBuilder) + + // Gathering the signatures. + val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) + } + + .. code-block:: java + + // Additional import. + import net.corda.flows.CollectSignaturesFlow; + + ... + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand); + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); + + // Signing the transaction. + final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Gathering the signatures. + final SignedTransaction signedTx = subFlow( + new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); + } + +``CollectSignaturesFlow`` gathers signatures from every participant listed on the transaction, and returns a +``SignedTransaction`` with all the required signatures. + +Finalising the transaction +^^^^^^^^^^^^^^^^^^^^^^^^^^ +We now have a valid transaction signed by all the required parties. All that's left to do is to have it notarised and +recorded by all the relevant parties. From then on, it will become a permanent part of the ledger. Again, instead +of handling this process manually, we'll use a built-in flow called ``FinalityFlow``: + +.. container:: codeset + + .. code-block:: kotlin + + // Additional import. + import net.corda.flows.FinalityFlow + + ... + + @Suspendable + override fun call(): SignedTransaction { + // We create a transaction builder + val txBuilder = TransactionBuilder() + val notaryIdentity = serviceHub.networkMapCache.getAnyNotary() + txBuilder.notary = notaryIdentity + + // We create the transaction's components. + val ourIdentity = serviceHub.myInfo.legalIdentity + val iou = IOUState(iouValue, ourIdentity, otherParty) + val txCommand = Command(IOUContract.Create(), iou.participants.map { it.owningKey }) + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand) + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(serviceHub).verify() + + // Signing the transaction. + val partSignedTx = serviceHub.signInitialTransaction(txBuilder) + + // Gathering the signatures. + val signedTx = subFlow(CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.tracker())) + + // Finalising the transaction. + return subFlow(FinalityFlow(signedTx)).single() + } + + .. code-block:: java + + // Additional import. + import net.corda.flows.FinalityFlow; + + ... + + @Suspendable + @Override + public SignedTransaction call() throws FlowException { + // We create a transaction builder + final TransactionBuilder txBuilder = new TransactionBuilder(); + final Party notary = getServiceHub().getNetworkMapCache().getAnyNotary(null); + txBuilder.setNotary(notary); + + // We create the transaction's components. + final Party ourIdentity = getServiceHub().getMyInfo().getLegalIdentity(); + final IOUState iou = new IOUState(iouValue, ourIdentity, otherParty); + final List signers = ImmutableList.of(ourIdentity.getOwningKey(), otherParty.getOwningKey()); + final Command txCommand = new Command(new IOUContract.Create(), signers); + + // Adding the item's to the builder. + txBuilder.withItems(iou, txCommand); + + // Verifying the transaction. + txBuilder.toWireTransaction().toLedgerTransaction(getServiceHub()).verify(); + + // Signing the transaction. + final SignedTransaction partSignedTx = getServiceHub().signInitialTransaction(txBuilder); + + // Gathering the signatures. + final SignedTransaction signedTx = subFlow( + new CollectSignaturesFlow(partSignedTx, CollectSignaturesFlow.Companion.tracker())); + + // Finalising the transaction. + return subFlow(new FinalityFlow(signedTx)).get(0); + } + +``FinalityFlow`` completely automates the process of: + +* Notarising the transaction +* Recording it in our vault +* Sending it to the counterparty for them to record as well + +``FinalityFlow`` also returns a list of the notarised transactions. We extract the single item from this list and +return it. + +That completes the initiator side of the flow. + +Writing the acceptor's flow +--------------------------- +The acceptor's side of the flow is much simpler. We need to: + +1. Receive a signed transaction from the counterparty +2. Verify the transaction +3. Sign the transaction +4. Send the updated transaction back to the counterparty + +As we just saw, the process of building and finalising the transaction will be completely handled by the initiator flow. + +SignTransactionFlow +~~~~~~~~~~~~~~~~~~~ +We can automate all four steps of the acceptor's flow by invoking ``SignTransactionFlow``. ``SignTransactionFlow`` is +a flow that is registered by default on every node to respond to messages from ``CollectSignaturesFlow`` (which is +invoked by the initiator flow). + +As ``SignTransactionFlow`` is an abstract class, we have to subclass it and override +``SignTransactionFlow.checkTransaction``: + +.. container:: codeset + + .. code-block:: kotlin + + // Additional import. + import net.corda.flows.SignTransactionFlow + + ... + + @InitiatedBy(Initiator::class) + class Acceptor(val otherParty: Party) : FlowLogic() { + + @Suspendable + override fun call() { + // Stage 1 - Verifying and signing the transaction. + subFlow(object : SignTransactionFlow(otherParty, tracker()) { + override fun checkTransaction(stx: SignedTransaction) { + // Define custom verification logic here. + } + }) + } + } + + .. code-block:: java + + // Additional import. + import net.corda.flows.SignTransactionFlow; + + ... + + @InitiatedBy(Initiator.class) + public static class Acceptor extends FlowLogic { + + private final Party otherParty; + + public Acceptor(Party otherParty) { + this.otherParty = otherParty; + } + + @Suspendable + @Override + public Void call() throws FlowException { + // Stage 1 - Verifying and signing the transaction. + + class signTxFlow extends SignTransactionFlow { + private signTxFlow(Party otherParty, ProgressTracker progressTracker) { + super(otherParty, progressTracker); + } + + @Override + protected void checkTransaction(SignedTransaction signedTransaction) { + // Define custom verification logic here. + } + } + + subFlow(new signTxFlow(otherParty, SignTransactionFlow.Companion.tracker())); + + return null; + } + } + +``SignTransactionFlow`` already checks the transaction's signatures, and whether the transaction is contractually +valid. The purpose of ``SignTransactionFlow.checkTransaction`` is to define any additional verification of the +transaction that we wish to perform before we sign it. For example, we may want to: + +* Check that the transaction contains an ``IOUState`` +* Check that the IOU's value isn't too high + +Well done! You've finished the flows! + +Flow tests +---------- +As with contracts, deploying nodes to manually test flows is not efficient. Instead, we can use Corda's flow-test +DSL to quickly test our flows. The flow-test DSL works by creating a network of lightweight, "mock" node +implementations on which we run our flows. + +The first thing we need to do is create this mock network. Open either ``test/kotlin/com/template/flow/FlowTests.kt`` or +``test/java/com/template/contract/ContractTests.java``, and overwrite the existing code with: + +.. container:: codeset + + .. code-block:: kotlin + + package com.template + + import net.corda.core.contracts.TransactionVerificationException + import net.corda.core.getOrThrow + import net.corda.testing.node.MockNetwork + import net.corda.testing.node.MockNetwork.MockNode + import org.junit.After + import org.junit.Before + import org.junit.Test + import kotlin.test.assertEquals + import kotlin.test.assertFailsWith + + class IOUFlowTests { + lateinit var net: MockNetwork + lateinit var a: MockNode + lateinit var b: MockNode + lateinit var c: MockNode + + @Before + fun setup() { + net = MockNetwork() + val nodes = net.createSomeNodes(2) + a = nodes.partyNodes[0] + b = nodes.partyNodes[1] + b.registerInitiatedFlow(IOUFlow.Acceptor::class.java) + net.runNetwork() + } + + @After + fun tearDown() { + net.stopNodes() + } + } + + .. code-block:: java + + package com.template; + + import com.google.common.collect.ImmutableList; + import com.google.common.util.concurrent.ListenableFuture; + import net.corda.core.contracts.ContractState; + import net.corda.core.contracts.TransactionState; + import net.corda.core.contracts.TransactionVerificationException; + import net.corda.core.transactions.SignedTransaction; + import net.corda.testing.node.MockNetwork; + import net.corda.testing.node.MockNetwork.BasketOfNodes; + import net.corda.testing.node.MockNetwork.MockNode; + import org.junit.After; + import org.junit.Before; + import org.junit.Rule; + import org.junit.Test; + import org.junit.rules.ExpectedException; + + import java.util.List; + + import static org.hamcrest.CoreMatchers.instanceOf; + import static org.junit.Assert.assertEquals; + + public class IOUFlowTests { + private MockNetwork net; + private MockNode a; + private MockNode b; + + @Before + public void setup() { + net = new MockNetwork(); + BasketOfNodes nodes = net.createSomeNodes(2); + a = nodes.getPartyNodes().get(0); + b = nodes.getPartyNodes().get(1); + b.registerInitiatedFlow(IOUFlow.Acceptor.class); + net.runNetwork(); + } + + @After + public void tearDown() { + net.stopNodes(); + } + + @Rule + public final ExpectedException exception = ExpectedException.none(); + } + +This creates an in-memory network with mocked-out components. The network has two nodes, plus network map and notary +nodes. We register any responder flows (in our case, ``IOUFlow.Acceptor``) on our nodes as well. + +Our first test will be to check that the flow rejects invalid IOUs: + +.. container:: codeset + + .. code-block:: kotlin + + @Test + fun `flow rejects invalid IOUs`() { + val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + + // The IOUContract specifies that IOUs cannot have negative values. + assertFailsWith {future.getOrThrow()} + } + + .. code-block:: java + + @Test + public void flowRejectsInvalidIOUs() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + + exception.expectCause(instanceOf(TransactionVerificationException.class)); + future.get(); + } + +This code causes node A to run the ``IOUFlow.Initiator`` flow. The call to ``MockNetwork.runNetwork`` is required to +simulate the running of a real network. + +We then assert that because we passed in a negative IOU value to the flow's constructor, the flow should fail with a +``TransactionVerificationException``. In other words, we are asserting that at some point in flow, the transaction is +verified (remember that ``IOUContract`` forbids negative value IOUs), causing the flow to fail. + +Because flows need to be instrumented by a library called `Quasar `_ that +allows the flows to be checkpointed and serialized to disk, you need to run these tests using the provided +``Run Flow Tests - Java`` or ``Run Flow Tests - Kotlin`` run-configurations. + +Here is the full suite of tests we'll use for the ``IOUFlow``: + +.. container:: codeset + + .. code-block:: kotlin + + @Test + fun `flow rejects invalid IOUs`() { + val flow = IOUFlow.Initiator(-1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + + // The IOUContract specifies that IOUs cannot have negative values. + assertFailsWith {future.getOrThrow()} + } + + @Test + fun `SignedTransaction returned by the flow is signed by the initiator`() { + val flow = IOUFlow.Initiator(1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + + val signedTx = future.getOrThrow() + signedTx.verifySignatures(b.services.legalIdentityKey) + } + + @Test + fun `SignedTransaction returned by the flow is signed by the acceptor`() { + val flow = IOUFlow.Initiator(1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + + val signedTx = future.getOrThrow() + signedTx.verifySignatures(a.services.legalIdentityKey) + } + + @Test + fun `flow records a transaction in both parties' vaults`() { + val flow = IOUFlow.Initiator(1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + val signedTx = future.getOrThrow() + + // We check the recorded transaction in both vaults. + for (node in listOf(a, b)) { + assertEquals(signedTx, node.storage.validatedTransactions.getTransaction(signedTx.id)) + } + } + + @Test + fun `recorded transaction has no inputs and a single output, the input IOU`() { + val flow = IOUFlow.Initiator(1, b.info.legalIdentity) + val future = a.services.startFlow(flow).resultFuture + net.runNetwork() + val signedTx = future.getOrThrow() + + // We check the recorded transaction in both vaults. + for (node in listOf(a, b)) { + val recordedTx = node.storage.validatedTransactions.getTransaction(signedTx.id) + val txOutputs = recordedTx!!.tx.outputs + assert(txOutputs.size == 1) + + val recordedState = txOutputs[0].data as IOUState + assertEquals(recordedState.value, 1) + assertEquals(recordedState.sender, a.info.legalIdentity) + assertEquals(recordedState.recipient, b.info.legalIdentity) + } + } + + .. code-block:: java + + @Test + public void flowRejectsInvalidIOUs() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(-1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + + exception.expectCause(instanceOf(TransactionVerificationException.class)); + future.get(); + } + + @Test + public void signedTransactionReturnedByTheFlowIsSignedByTheInitiator() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + + SignedTransaction signedTx = future.get(); + signedTx.verifySignatures(b.getServices().getLegalIdentityKey()); + } + + @Test + public void signedTransactionReturnedByTheFlowIsSignedByTheAcceptor() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + + SignedTransaction signedTx = future.get(); + signedTx.verifySignatures(a.getServices().getLegalIdentityKey()); + } + + @Test + public void flowRecordsATransactionInBothPartiesVaults() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + SignedTransaction signedTx = future.get(); + + for (MockNode node : ImmutableList.of(a, b)) { + assertEquals(signedTx, node.storage.getValidatedTransactions().getTransaction(signedTx.getId())); + } + } + + @Test + public void recordedTransactionHasNoInputsAndASingleOutputTheInputIOU() throws Exception { + IOUFlow.Initiator flow = new IOUFlow.Initiator(1, b.info.getLegalIdentity()); + ListenableFuture future = a.getServices().startFlow(flow).getResultFuture(); + net.runNetwork(); + SignedTransaction signedTx = future.get(); + + for (MockNode node : ImmutableList.of(a, b)) { + SignedTransaction recordedTx = node.storage.getValidatedTransactions().getTransaction(signedTx.getId()); + List> txOutputs = recordedTx.getTx().getOutputs(); + assert(txOutputs.size() == 1); + + IOUState recordedState = (IOUState) txOutputs.get(0).getData(); + assert(recordedState.getValue() == 1); + assertEquals(recordedState.getSender(), a.info.getLegalIdentity()); + assertEquals(recordedState.getRecipient(), b.info.getLegalIdentity()); + } + } + +Run these tests and make sure they all pass. If they do, its very likely that we have a working CorDapp. + +Progress so far +--------------- +We now have a flow that we can kick off on our node to completely automate the process of issuing an IOU onto the +ledger. Under the hood, this flow takes the form of two communicating ``FlowLogic`` subclasses. + +We now have a complete CorDapp, made up of: + +* The ``IOUState``, representing IOUs on the ledger +* The ``IOUContract``, controlling the evolution of ``IOUState`` objects over time +* The ``IOUFlow``, which transforms the creation of a new IOU on the ledger into a push-button process + +The final step is to spin up some nodes and test our CorDapp. \ No newline at end of file diff --git a/docs/source/hello-world-index.rst b/docs/source/hello-world-index.rst new file mode 100644 index 0000000000..5b2b7f8c74 --- /dev/null +++ b/docs/source/hello-world-index.rst @@ -0,0 +1,12 @@ +Hello, World! +============= + +.. toctree:: + :maxdepth: 1 + + hello-world-introduction + hello-world-template + hello-world-state + hello-world-contract + hello-world-flow + hello-world-running \ No newline at end of file diff --git a/docs/source/hello-world-introduction.rst b/docs/source/hello-world-introduction.rst new file mode 100644 index 0000000000..d66ffeb272 --- /dev/null +++ b/docs/source/hello-world-introduction.rst @@ -0,0 +1,63 @@ +Introduction +============ + +By this point, :doc:`your dev environment should be set up `, you've run +:doc:`your first CorDapp `, and you're familiar with Corda's :doc:`key concepts `. What +comes next? + +If you're a developer, the next step is to write your own CorDapp. Each CorDapp takes the form of a plugin that is +installed on one or more Corda nodes, and gives them the ability to conduct some new process - anything from +issuing a debt instrument to making a restaurant booking. + +Our use-case +------------ +Our CorDapp will seek to model IOUs on ledger. An IOU – short for “I Owe yoU” – records the fact that one person owes +another a given amount of money. We can imagine that this is potentially sensitive information that we'd only want to +communicate on a need-to-know basis. This is one of the areas where Corda excels - allowing a small set of parties to +agree on a fact without needing to share this fact with everyone else on the network, as you do with most other +blockchain platforms. + +To serve any useful function, a CorDapp needs three core elements: + +* **One or more states** – the shared facts that will be agreed upon and stored on the ledger +* **One or more contracts** – the rules governing how these states can evolve over time +* **One or more flows** – the step-by-step process for carrying out a ledger update + +Our IOU CorDapp is no exception. It will have the following elements: + +State +^^^^^ +The states will be IOUStates, with each instance representing a single IOU. We can visualize an IOUState as follows: + + .. image:: resources/tutorial-state.png + :scale: 25% + :align: center + +Contract +^^^^^^^^ +Our contract will be the IOUContract, imposing rules on the evolution of IOUs over time: + + * Only the creation of new IOUs will be allowed + * Transferring existing IOUs or paying off an IOU with cash will not be allowed + +However, we can easily extend our CorDapp to handle additional use-cases later on. + +Flow +^^^^ +Our flow will be the IOUFlow. It will allow two nodes to orchestrate the creation of a new IOU on the ledger, via the +following steps: + + .. image:: resources/tutorial-flow.png + :scale: 25% + :align: center + +In traditional distributed ledger systems, where all data is broadcast to every network participant, you don’t even +think about this step – you simply package up your ledger update and send it out into the world. But in Corda, where +privacy is a core focus, flows are used to carefully control who sees what during the process of agreeing a +ledger update. + +Progress so far +--------------- +We've sketched out a simple CorDapp that will allow nodes to confidentially agree the creation of new IOUs. + +Next, we'll be taking a look at the template project we'll be using as a base for our work. \ No newline at end of file diff --git a/docs/source/hello-world-running.rst b/docs/source/hello-world-running.rst new file mode 100644 index 0000000000..8e604f8d11 --- /dev/null +++ b/docs/source/hello-world-running.rst @@ -0,0 +1,208 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Running our CorDapp +=================== + +Now that we've written a CorDapp, it's time to test it by running it on some real Corda nodes. + +Deploying our CorDapp +--------------------- +Let's take a look at the nodes we're going to deploy. Open the project's build file under ``java-source/build.gradle`` +or ``kotlin-source/build.gradle`` and scroll down to the ``task deployNodes`` section. This section defines four +nodes - the Controller, and NodeA, NodeB and NodeC: + +.. container:: codeset + + .. code-block:: kotlin + + task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['build']) { + directory "./build/nodes" + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" + node { + name "CN=Controller,O=R3,OU=corda,L=London,C=GB" + advertisedServices = ["corda.notary.validating"] + p2pPort 10002 + rpcPort 10003 + webPort 10004 + cordapps = [] + } + node { + name "CN=NodeA,O=NodeA,L=London,C=GB" + advertisedServices = [] + p2pPort 10005 + rpcPort 10006 + webPort 10007 + cordapps = [] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + node { + name "CN=NodeB,O=NodeB,L=New York,C=US" + advertisedServices = [] + p2pPort 10008 + rpcPort 10009 + webPort 10010 + cordapps = [] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + node { + name "CN=NodeC,O=NodeC,L=Paris,C=FR" + advertisedServices = [] + p2pPort 10011 + rpcPort 10012 + webPort 10013 + cordapps = [] + rpcUsers = [[ user: "user1", "password": "test", "permissions": []]] + } + } + +We have three standard nodes, plus a special Controller node that is running the network map service, and is also +advertising a validating notary service. Feel free to add additional node definitions here to expand the size of the +test network. + +We can run this ``deployNodes`` task using Gradle. For each node definition, Gradle will: + +* Package the project's source files into a CorDapp jar +* Create a new node in ``build/nodes`` with our CorDapp already installed + +We can do that now by running the following commands from the root of the project: + +.. code:: python + + // On Windows + gradlew clean deployNodes + + // On Mac + ./gradlew clean deployNodes + +Running the nodes +----------------- +Running ``deployNodes`` will build the nodes under both ``java-source/build/nodes`` and ``kotlin-source/build/nodes``. +If we navigate to one of these folders, we'll see four node folder. Each node folder has the following structure: + + .. code:: python + + . + // The runnable node + |____corda.jar + // The node's webserver + |____corda-webserver.jar + |____dependencies + // The node's configuration file + |____node.conf + |____plugins + // Our IOU CorDapp + |____java/kotlin-source-0.1.jar + +Let's start the nodes by running the following commands from the root of the project: + +.. code:: python + + // On Windows for a Java CorDapp + java-source/build/nodes/runnodes.bat + + // On Windows for a Kotlin CorDapp + kotlin-source/build/nodes/runnodes.bat + + // On Mac for a Java CorDapp + java-source/build/nodes/runnodes + + // On Mac for a Kotlin CorDapp + kotlin-source/build/nodes/runnodes + +This will start a terminal window for each node, and an additional terminal window for each node's webserver - eight +terminal windows in all. Give each node a moment to start - you'll know it's ready when its terminal windows displays +the message, "Welcome to the Corda interactive shell.". + + .. image:: resources/running_node.png + :scale: 25% + :align: center + +Interacting with the nodes +-------------------------- +Now that our nodes are running, let's order one of them to create an IOU by kicking off our ``IOUFlow``. In a larger +app, we'd generally provide a web API sitting on top of our node. Here, for simplicity, we'll be interacting with the +node via its built-in CRaSH shell. + +Go to the terminal window displaying the CRaSH shell of Node A. Typing ``help`` will display a list of the available +commands. + +We want to create an IOU of 100 with Node B. We start the ``IOUFlow`` by typing: + +.. code:: python + + start IOUFlow arg0: 99, arg1: "CN=NodeB,O=NodeB,L=New York,C=US" + +Node A and Node B will automatically agree an IOU. + +If the flow worked, it should have led to the recording of a new IOU in the vaults of both Node A and Node B. Equally +importantly, Node C - although it sits on the same network - should not be aware of this transaction. + +We can check the flow has worked by using an RPC operation to check the contents of each node's vault. Typing ``run`` +will display a list of the available commands. We can examine the contents of a node's vault by running: + +.. code:: python + + run vaultAndUpdates + +And we can also examine a node's transaction storage, by running: + +.. code:: python + + run verifiedTransactions + +The vaults of Node A and Node B should both display the following output: + +.. code:: python + + first: + - state: + data: + value: 99 + sender: "CN=NodeA,O=NodeA,L=London,C=GB" + recipient: "CN=NodeB,O=NodeB,L=New York,C=US" + contract: + legalContractReference: "559322B95BCF7913E3113962DC3F3CBD71C818C66977721580C045DC41C813A5" + participants: + - "CN=NodeA,O=NodeA,L=London,C=GB" + - "CN=NodeB,O=NodeB,L=New York,C=US" + notary: "CN=Controller,O=R3,OU=corda,L=London,C=GB,OU=corda.notary.validating" + encumbrance: null + ref: + txhash: "656A1BF64D5AEEC6F6C944E287F34EF133336F5FC2C5BFB9A0BFAE25E826125F" + index: 0 + second: "(observable)" + +But the vault of Node C should output nothing! + +.. code:: python + + first: [] + second: "(observable)" + +Conclusion +---------- +We have written a simple CorDapp that allows IOUs to be issued onto the ledger. Like all CorDapps, our +CorDapp is made up of three key parts: + +* The ``IOUState``, representing IOUs on the ledger +* The ``IOUContract``, controlling the evolution of IOUs over time +* The ``IOUFlow``, orchestrating the process of agreeing the creation of an IOU on-ledger. + +Together, these three parts completely determine how IOUs are created and evolved on the ledger. + +Next steps +---------- +You should now be ready to develop your own CorDapps. There's +`a more fleshed-out version of the IOU CorDapp `_ +with an API and web front-end, and a set of example CorDapps in +`the main Corda repo `_, under ``samples``. An explanation of how to run these +samples :doc:`here `. + +As you write CorDapps, you can learn more about the API available :doc:`here `. + +If you get stuck at any point, please reach out on `Slack `_, +`Discourse `_, or `Stack Overflow `_. \ No newline at end of file diff --git a/docs/source/hello-world-state.rst b/docs/source/hello-world-state.rst new file mode 100644 index 0000000000..7196bcbd7a --- /dev/null +++ b/docs/source/hello-world-state.rst @@ -0,0 +1,163 @@ +.. highlight:: kotlin +.. raw:: html + + + + +Writing the state +================= + +In Corda, shared facts on the ledger are represented as states. Our first task will be to define a new state type to +represent an IOU. + +The ContractState interface +--------------------------- +In Corda, any JVM class that implements the ``ContractState`` interface is a valid state. ``ContractState`` is +defined as follows: + +.. container:: codeset + + .. code-block:: kotlin + + interface ContractState { + // The contract that imposes constraints on how this state can evolve over time. + val contract: Contract + + // The list of entities considered to have a stake in this state. + val participants: List + } + +The first thing you'll probably notice about this interface declaration is that its not written in Java or another +common language. The core Corda platform, including the interface declaration above, is entirely written in Kotlin. + +Learning some Kotlin will be very useful for understanding how Corda works internally, and usually only takes an +experienced Java developer a day or so to pick up. However, learning Kotlin isn't essential. Because Kotlin code +compiles down to JVM bytecode, CorDapps written in other JVM languages can interoperate with Corda. + +If you do want to dive into Kotlin, there's an official +`getting started guide `_, and a series of +`Kotlin Koans `_. + +If not, here's a quick primer on the Kotlinisms in the declaration of ``ContractState``: + +* ``val`` declares a read-only property, similar to Java's ``final`` keyword +* The syntax ``varName: varType`` declares ``varName`` as being of type ``varType`` + +We can see that the ``ContractState`` interface declares two properties: + +* ``contract``: the contract controlling transactions involving this state +* ``participants``: the list of entities that have to approve state changes such as changing the state's notary or + upgrading the state's contract + +Beyond this, our state is free to define any properties, methods, helpers or inner classes it requires to accurately +represent a given class of shared facts on the ledger. + +``ContractState`` also has several child interfaces that you may wish to implement depending on your state, such as +``LinearState`` and ``OwnableState``. + +Modelling IOUs +-------------- +How should we define the ``IOUState`` representing IOUs on the ledger? Beyond implementing the ``ContractState`` +interface, our ``IOUState`` will also need properties to track the relevant features of the IOU: + +* The sender of the IOU +* The IOU's recipient +* The value of the IOU + +There are many more fields you could include, such as the IOU's currency. We'll abstract them away for now. If +you wish to add them later, its as simple as adding an additional property to your class definition. + +Defining IOUState +----------------- +Let's open ``TemplateState.java`` (for Java) or ``TemplateState.kt`` (for Kotlin) and update ``TemplateState`` to +define an ``IOUState``: + +.. container:: codeset + + .. code-block:: kotlin + + package com.template + + import net.corda.core.contracts.ContractState + import net.corda.core.identity.Party + + class IOUState(val value: Int, + val sender: Party, + val recipient: Party, + // TODO: Once we've defined IOUContract, come back and update this. + override val contract: TemplateContract = TemplateContract()) : ContractState { + + override val participants get() = listOf(sender, recipient) + } + + .. code-block:: java + + package com.template; + + import com.google.common.collect.ImmutableList; + import net.corda.core.contracts.ContractState; + import net.corda.core.identity.AbstractParty; + import net.corda.core.identity.Party; + + import java.util.List; + + public class IOUState implements ContractState { + private final int value; + private final Party sender; + private final Party recipient; + // TODO: Once we've defined IOUContract, come back and update this. + private final TemplateContract contract; + + public IOUState(int value, Party sender, Party recipient, IOUContract contract) { + this.value = value; + this.sender = sender; + this.recipient = recipient; + this.contract = contract; + } + + public int getValue() { + return value; + } + + public Party getSender() { + return sender; + } + + public Party getRecipient() { + return recipient; + } + + @Override + // TODO: Once we've defined IOUContract, come back and update this. + public TemplateContract getContract() { + return contract; + } + + @Override + public List getParticipants() { + return ImmutableList.of(sender, recipient); + } + } + +We've made the following changes: + +* We've renamed ``TemplateState`` to ``IOUState`` +* We've added properties for ``value``, ``sender`` and ``recipient`` (along with any getters and setters in Java): + + * ``value`` is just a standard int (in Java)/Int (in Kotlin), but ``sender`` and ``recipient`` are of type + ``Party``. ``Party`` is a built-in Corda type that represents an entity on the network. + +* We've overridden ``participants`` to return a list of the ``sender`` and ``recipient`` +* This means that actions such as changing the state's contract or its notary will require approval from both the + ``sender`` and the ``recipient`` + +We've left ``IOUState``'s contract as ``TemplateContract`` for now. We'll update this once we've defined the +``IOUContract``. + +Progress so far +--------------- +We've defined an ``IOUState`` that can be used to represent IOUs as shared facts on the ledger. As we've seen, states in +Corda are simply JVM classes that implement the ``ContractState`` interface. They can have any additional properties and +methods you like. + +Next, we'll be writing our ``IOUContract`` to control the evolution of these shared facts over time. \ No newline at end of file diff --git a/docs/source/hello-world-template.rst b/docs/source/hello-world-template.rst new file mode 100644 index 0000000000..c298fb87c0 --- /dev/null +++ b/docs/source/hello-world-template.rst @@ -0,0 +1,85 @@ +.. highlight:: kotlin +.. raw:: html + + + + +The CorDapp Template +==================== + +When writing a new CorDapp, you’ll generally want to base it on the +`Cordapp Template `_. The Cordapp Template allows you to quickly deploy +your CorDapp onto a local test network of dummy nodes to evaluate its functionality. + +Note that there's no need to download and install Corda itself. As long as you're working from a stable Milestone +branch, the required libraries will be downloaded automatically from an online repository. + +If you do wish to work from the latest snapshot, please follow the instructions +`here `_. + +Downloading the template +------------------------ +Open a terminal window in the directory where you want to download the CorDapp template, and run the following commands: + +.. code-block:: text + + # Clone the template from GitHub: + git clone https://github.com/corda/cordapp-template.git & cd cordapp-template + + # Retrieve a list of the stable Milestone branches using: + git branch -a --list *release-M* + + # Check out the Milestone branch with the latest version number: + git checkout release-M[*version number*] & git pull + +Template structure +------------------ +We can write our CorDapp in either Java or Kotlin, and will be providing the code in both languages throughout. If +you want to write the CorDapp in Java, you'll be modifying the files under ``java-source``. If you prefer to use +Kotlin, you'll be modifying the files under ``kotlin-source``. + +To implement our IOU CorDapp, we'll only need to modify five files: + +.. container:: codeset + + .. code-block:: java + + // 1. The state + java-source/src/main/java/com/template/state/TemplateState.java + + // 2. The contract + java-source/src/main/java/com/template/contract/TemplateContract.java + + // 3. The flow + java-source/src/main/java/com/template/flow/TemplateFlow.java + + // Tests for our contract and flow: + // 1. The contract tests + java-source/src/test/java/com/template/contract/ContractTests.java + + // 2. The flow tests + java-source/src/test/java/com/template/flow/FlowTests.java + + .. code-block:: kotlin + + // 1. The state + kotlin-source/src/main/kotlin/com/template/state/TemplateState.kt + + // 2. The contract + kotlin-source/src/main/kotlin/com/template/contract/TemplateContract.kt + + // 3. The flow + kotlin-source/src/main/kotlin/com/template/flow/TemplateFlow.kt + + // Tests for our contract and flow: + // 1. The contract tests + kotlin-source/src/test/kotlin/com/template/contract/ContractTests.kt + + // 2. The flow tests + kotlin-source/src/test/kotlin/com/template/flow/FlowTests.kt + +Progress so far +--------------- +We now have a template that we can build upon to define our IOU CorDapp. + +We'll begin writing the CorDapp proper by writing the definition of the ``IOUState``. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 0fa0e49946..fab9e1ec77 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,147 +1,43 @@ -Welcome to the Corda documentation! -=================================== +Welcome to Corda ! +================== -.. 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 M11.1 `_. +.. warningX:: 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 M12.1 `_. -`Corda `_ is an open-source distributed ledger platform. The latest *milestone* (i.e. stable) -release is M11.1. The codebase is on `GitHub `_, and our community can be found on -`Slack `_ and in our `forum `_. +`Corda `_ is a blockchain-inspired open source distributed ledger platform. If you’d like a +quick introduction to distributed ledgers and how Corda is different, then watch this short video: -If you're new to Corda, you should start by learning about its motivating vision and architecture. A good introduction -is the `Introduction to Corda webinar `_ and the `Introductory white paper`_. As -you become more familiar with Corda, readers with a technical background will also want to dive into the `Technical white paper`_, -which describes the platform's envisioned end-state. +.. raw:: html -.. note:: Corda training is now available in London, New York and Singapore! `Learn more. `_ + + + -Corda is designed so that developers can easily extend its functionality by writing CorDapps -(**Cor**\ da **D**\ istributed **App**\ lication\ **s**\ ). Some example CorDapps are available in the Corda repo's -`samples `_ directory. To run these yourself, make -sure you follow the instructions in :doc:`getting-set-up`, then go to -:doc:`running-the-demos`. +Want to see Corda running? Download our demonstration application `DemoBench `_ or follow our :doc:`quickstart guide `. -If, after running the demos, you're interested in writing your own CorDapps, you can use the -`CorDapp template `_ as a base. A simple example CorDapp built upon the template is available `here `_, and a video primer on basic CorDapp structure is available `here `_. +If you want to start coding on Corda, then familiarise yourself with the :doc:`key concepts `, then read our :doc:`Hello, World! tutorial `. For the background behind Corda, read the non-technical `introductory white paper`_ or for more detail, the `technical white paper`_. -From there, you'll be in a position to start extending the example CorDapp yourself (e.g. by writing new states, contracts, -and/or flows). For this, you'll want to refer to this docsite, and to the `tutorials `_ -in particular. If you get stuck, get in touch on `Slack `_ or the `forum `_. +If you have questions or comments, then get in touch with us either on `Slack `_, `Discourse `_, or write a question on `stackoverflow `_ . -Once you're familiar with Corda and CorDapp development, we'd encourage you to get involved in the development of the -platform itself. Find out more about `contributing to Corda `_. +We look forward to seeing what you can do with Corda! -.. _`Introductory white paper`: _static/corda-introductory-whitepaper.pdf -.. _`Technical white paper`: _static/corda-technical-whitepaper.pdf - -Documentation Contents: -======================= +.. _`introductory white paper`: _static/corda-introductory-whitepaper.pdf +.. _`technical white paper`: _static/corda-technical-whitepaper.pdf .. toctree:: - :maxdepth: 2 - :caption: Getting started + :maxdepth: 1 - inthebox - getting-set-up - getting-set-up-fault-finding - running-the-demos - CLI-vs-IDE - cheat-sheet - -.. toctree:: - :maxdepth: 2 - :caption: Key concepts - - key-concepts - key-concepts-ecosystem - key-concepts-data-model - key-concepts-core-types - key-concepts-financial-model - key-concepts-flow-framework - key-concepts-consensus-notaries - key-concepts-vault - key-concepts-security-model - -.. toctree:: - :maxdepth: 2 - :caption: CorDapps - - creating-a-cordapp - tutorial-cordapp - -.. toctree:: - :maxdepth: 2 - :caption: The Corda node - - versioning - shell - serialization - clientrpc - messaging - persistence - node-administration - corda-configuration-file - corda-plugins - node-services - node-explorer - permissioning - -.. toctree:: - :maxdepth: 2 - :caption: Tutorials - - tutorial-contract - tutorial-contract-clauses - tutorial-test-dsl - contract-upgrade - tutorial-integration-testing - tutorial-clientrpc-api - tutorial-building-transactions - flow-state-machines - flow-testing - running-a-notary - using-a-notary - oracles - tutorial-attachments - event-scheduling - -.. toctree:: - :maxdepth: 2 - :caption: Other - - network-simulator - clauses - merkle-trees - json - -.. toctree:: - :maxdepth: 2 - :caption: Component library - - flow-library - contract-catalogue - contract-irs - -.. toctree:: - :maxdepth: 2 - :caption: Appendix - - loadtesting - demobench - setting-up-a-corda-network - secure-coding-guidelines - release-process - release-notes - changelog - codestyle - building-the-docs - further-notes-on-kotlin - publishing-corda - azure-vm - out-of-process-verification - -.. toctree:: - :maxdepth: 2 - :caption: Glossary - - glossary + quickstart-index.rst + key-concepts.rst + building-a-cordapp-index.rst + corda-nodes-index.rst + corda-networks-index.rst + tutorials-index.rst + tools-index.rst + node-internals-index.rst + component-library-index.rst + release-process-index.rst + faq.rst + troubleshooting.rst + other-index.rst + glossary.rst diff --git a/docs/source/inthebox.rst b/docs/source/inthebox.rst deleted file mode 100644 index 8531257252..0000000000 --- a/docs/source/inthebox.rst +++ /dev/null @@ -1,53 +0,0 @@ -What's included? -================ - -This Corda early access preview includes: - -* A collection of samples, for instance a web app demo that uses it to implement IRS trading. -* A template app you can use to get started, and tutorial app that teaches you the basics. -* A peer to peer network with message persistence and delivery retries. -* Key data structures for defining contracts and states. -* Smart contracts, which you can find in the :doc:`contract-catalogue`. -* API documentation and tutorials (what you're reading). -* A business process workflow framework. -* Notary infrastructure for precise timestamping, and elimination of double spending without a blockchain. -* A simple RPC API. -* A user interface for administration. - -Some things it does not currently include but should gain later are: - -* Sandboxing, distribution and publication of smart contract code. -* A well specified wire protocol. -* An identity framework. - -The open source version of Corda is designed for developers exploring how to write apps. It is not intended to -be production grade software. For example it uses an embedded SQL database and doesn't yet have connectivity -support for mainstream SQL vendors (Oracle, Postgres, MySQL, SQL Server etc). It hasn't been security audited -and the APIs change in every release. - -Source tree layout ------------------- - -The Corda repository comprises the following folders: - -* **buildSrc** contains necessary gradle plugins to build Corda. -* **client** contains libraries for connecting to a node, working with it remotely and binding server-side data to JavaFX UI. -* **config** contains logging configurations and the default node configuration file. -* **core** containing the core Corda libraries such as crypto functions, types for Corda's building blocks: states, - contracts, transactions, attachments, etc. and some interfaces for nodes and protocols. -* **docs** contains the Corda docsite in restructured text format as well as the built docs in html. The docs can be - accessed via ``/docs/index.html`` from the root of the repo. -* **finance** defines a range of elementary contracts (and associated schemas) and protocols, such as abstract fungible - assets, cash, obligation and commercial paper. -* **gradle** contains the gradle wrapper which you'll use to execute gradle commands. -* **gradle-plugins** contains some additional plugins which we use to deploy Corda nodes. -* **lib** contains some dependencies. -* **node** contains the core code of the Corda node (eg: node driver, node services, messaging, persistence). -* **node-api** contains data structures shared between the node and the client module, e.g. types sent via RPC. -* **node-schemas** contains entity classes used to represent relational database tables. -* **samples** contains all our Corda demos and code samples. -* **test-utils** contains some utilities for unit testing contracts ( the contracts testing DSL) and protocols (the - mock network) implementation. -* **tools** contains the explorer which is a GUI front-end for Corda, and also the DemoBench which is a GUI tool that allows you to run Corda nodes locally for demonstrations. -* **verifier** allows out-of-node transaction verification, allowing verification to scale horizontally. -* **webserver** is a servlet container for CorDapps that export HTTP endpoints. This server is an RPC client of the node. diff --git a/docs/source/key-concepts-consensus-notaries.rst b/docs/source/key-concepts-consensus-notaries.rst deleted file mode 100644 index 8b8a1efd0a..0000000000 --- a/docs/source/key-concepts-consensus-notaries.rst +++ /dev/null @@ -1,162 +0,0 @@ -Consensus and notaries -====================== - -A notary is a service that provides transaction ordering and timestamping. - -Notaries are expected to be composed of multiple mutually distrusting parties who use a standard consensus algorithm. -Notaries are identified by and sign with :ref:`composite-keys`. Notaries accept transactions submitted to them for processing -and either return a signature over the transaction, or a rejection error that states that a double spend attempt has occurred. - -Corda has "pluggable" notary services to improve privacy, scalability, legal-system compatibility and algorithmic agility. -The platform currently provides validating and non-validating notaries, and a distributed RAFT implementation. - -Consensus model ---------------- - -The fundamental unit of consensus in Corda is the **state**. Consensus can be divided into two parts: - -1. Consensus over state **validity** -- parties can reach certainty that a transaction is accepted by the contracts pointed - to by the input and output states, and has all the required signatures. This is achieved by parties independently running - the same contract code and validation logic (as described in :doc:`data model `) - -2. Consensus over state **uniqueness** -- parties can reach certainty the output states created in a transaction are the - unique successors to the input states consumed by that transaction (in other words -- an input state has not been previously - consumed) - -.. note:: The current model is still a **work in progress** and everything described in this article can and is likely to change - -Notary ------- - -A **notary** is an authority responsible for attesting that for a given transaction, it has not signed another transaction -consuming any of the same input states. Every **state** has an appointed notary: - -.. sourcecode:: kotlin - - /** - * A wrapper for [ContractState] containing additional platform-level state information. - * This is the definitive state that is stored on the ledger and used in transaction outputs - */ - data class TransactionState( - /** The custom contract state */ - val data: T, - /** Identity of the notary that ensures the state is not used as an input to a transaction more than once */ - val notary: Party) { - ... - } - -Transactions are signed by a notary to ensure their input states are **valid** (apart from *issue* transactions, containing no input states). -Furthermore, when using a validating notary, a transaction is only valid if all its dependencies are also valid. - -.. note:: The notary is a logical concept and can itself be a distributed entity, potentially a cluster maintained by mutually distrusting parties - -When the notary is requested to sign a transaction, it either signs it, attesting that the outputs are the **unique** -successors of the inputs, or provides conflict information for any input state that has been consumed by another transaction -it has already signed. In doing so, the notary provides the point of finality in the system. Until the notary signature -is obtained, parties cannot be sure that an equally valid, but conflicting, transaction will not be regarded as confirmed. -After the signature is obtained, the parties know that the inputs to this transaction have been uniquely consumed by this transaction. -Hence, it is the point at which we can say finality has occurred. - -Multiple notaries -~~~~~~~~~~~~~~~~~ - -More than one notary can exist in a network. This gives the following benefits: - -* **Custom behaviour**. We can have both validating and privacy preserving Notaries -- parties can make a choice based - on their specific requirements. -* **Load balancing**. Spreading the transaction load over multiple notaries will allow higher transaction throughput in - the platform overall -* **Low latency**. Latency could be minimised by choosing a notary physically closer the transacting parties - -Changing notaries -~~~~~~~~~~~~~~~~~ - -A transaction should only be signed by a notary if all of its input states point to the same notary. -In cases where a transaction involves states controlled by multiple notaries, the states first have to be repointed to the same notary. -This is achieved by using a special type of transaction whose sole output state is identical to its sole input state except for its designated notary. -Ensuring that all input states point to the same notary is the responsibility of each involved party -(it is another condition for an output state of the transaction to be **valid**) - -To change the notary for an input state, use the ``NotaryChangeFlow``. For example: - -.. sourcecode:: kotlin - - @Suspendable - fun changeNotary(originalState: StateAndRef, - newNotary: Party): StateAndRef { - val flow = NotaryChangeFlow(originalState, newNotary) - return subFlow(flow) - } - -The flow will: - -1. Construct a transaction with the old state as the input and the new state as the output - -2. Obtain signatures from all *participants* (a participant is any party that is able to consume this state in a valid transaction, as defined by the state itself) - -3. Obtain the *old* notary signature - -4. Record and distribute the final transaction to the participants so that everyone possesses the new state - -.. note:: Eventually, changing notaries will be handled automatically on demand. - -Validation ----------- - -One of the design decisions for a notary is whether or not to **validate** a transaction before accepting it. - -If a transaction is not checked for validity, it opens the platform to "denial of state" attacks, where anyone can build -an invalid transaction consuming someone else's states and submit it to the notary to get the states blocked. However, -if the transaction is validated, this requires the notary to be able to see the full contents of the transaction in -question and its dependencies. This is an obvious privacy leak. - -The platform is flexible and currently supports both validating and non-validating notary implementations -- a -party can select which one to use based on its own privacy requirements. - -.. note:: In the non-validating model, the "denial of state" attack is partially alleviated by requiring the calling - party to authenticate and storing its identity for the request. The conflict information returned by the notary - specifies the consuming transaction ID along with the identity of the party that had created the transaction. If the - conflicting transaction is valid, the current one is aborted; if not, a dispute can be raised and the input states - of the conflicting invalid transaction are "un-committed" (via a legal process). - -Timestamping ------------- - -A notary can also act as a *timestamping authority*, verifying the transaction timestamp command. - -For a timestamp to be meaningful, its implications must be binding on the party requesting it. -A party can obtain a timestamp signature in order to prove that some event happened *before*, *on*, or *after* a particular point in time. -However, if the party is not also compelled to commit to the associated transaction, it has a choice of whether or not to reveal this fact until some point in the future. -As a result, we need to ensure that the notary either has to also sign the transaction within some time tolerance, -or perform timestamping *and* notarisation at the same time, which is the chosen behaviour for this model. - -There will never be exact clock synchronisation between the party creating the transaction and the notary. -This is not only due to physics, network latencies, etc. but also because between inserting the command and getting the -notary to sign there may be many other steps, like sending the transaction to other parties involved in the trade, or -even requesting human sign-off. Thus the time observed by the notary may be quite different to the time observed by the -party creating the transaction. - -For this reason, times in transactions are specified as time *windows*, not absolute times. -In a distributed system there can never be "true time", only an approximation of it. Time windows can be -open-ended (i.e. specify only one of "before" and "after") or they can be fully bounded. If a time window needs to -be converted to an absolute time (e.g. for display purposes), there is a utility method on ``Timestamp`` to -calculate the mid point. - -In this way, we express the idea that the *true value* of the fact "the current time" is actually unknowable. Even when both before and -after times are included, the transaction could have occurred at any point between those two timestamps. Here, -"occurrence" could mean the execution date, the value date, the trade date etc ... The notary doesn't care what precise -meaning the timestamp has to the contract. - -By creating a range that can be either closed or open at one end, we allow all of the following facts to be modelled: - -* This transaction occurred at some point after the given time (e.g. after a maturity event) -* This transaction occurred at any time before the given time (e.g. before a bankruptcy event) -* This transaction occurred at some point roughly around the given time (e.g. on a specific day) - -.. note:: It is assumed that the time feed for a notary is GPS/NaviStar time as defined by the atomic - clocks at the US Naval Observatory. This time feed is extremely accurate and available globally for free. - -Also see section 7 of the `Technical white paper`_ which covers this topic in significantly more depth. - -.. _`Technical white paper`: _static/corda-technical-whitepaper.pdf - diff --git a/docs/source/key-concepts-consensus.rst b/docs/source/key-concepts-consensus.rst new file mode 100644 index 0000000000..d679dc1f45 --- /dev/null +++ b/docs/source/key-concepts-consensus.rst @@ -0,0 +1,73 @@ +Consensus +========= + +.. topic:: Summary + + * *To be committed, transactions must achieve both validity and uniqueness consensus* + * *Validity consensus requires contractual validity of the transaction and all its dependencies* + * *Uniqueness consensus prevents double-spends* + +Video +----- +.. raw:: html + + +

+ +Two types of consensus +---------------------- +Determining whether a proposed transaction is a valid ledger update involves reaching two types of consensus: + +* *Validity consensus* - this is checked by each required signer before they sign the transaction +* *Uniqueness consensus* - this is only checked by a notary service + +Validity consensus +------------------ +Validity consensus is the process of checking that the following conditions hold both for the proposed transaction, +and for every transaction in the transaction chain that generated the inputs to the proposed transaction: + +* The transaction is accepted by the contracts of every input and output state +* The transaction has all the required signatures + +It is not enough to verify the proposed transaction itself. We must also verify every transaction in the chain of +transactions that led up to the creation of the inputs to the proposed transaction. + +This is known as *walking the chain*. Suppose, for example, that a party on the network proposes a transaction +transferring us a treasury bond. We can only be sure that the bond transfer is valid if: + +* The treasury bond was issued by the central bank in a valid issuance transaction +* Every subsequent transaction in which the bond changed hands was also valid + +The only way to be sure of both conditions is to walk the transaction's chain. We can visualize this process as follows: + +.. image:: resources/validation-consensus.png + :scale: 25% + :align: center + +When verifying a proposed transaction, a given party may not have every transaction in the transaction chain that they +need to verify. In this case, they can request the missing transactions from the transaction proposer(s). The +transaction proposer(s) will always have the full transaction chain, since they would have requested it when +verifying the transaction that created the proposed transaction's input states. + +Uniqueness consensus +-------------------- +Imagine that Bob holds a valid central-bank-issued cash state of $1,000,000. Bob can now create two transaction +proposals: + +* A transaction transferring the $1,000,000 to Charlie in exchange for £800,000 +* A transaction transferring the $1,000,000 to Dan in exchange for €900,000 + +This is a problem because, although both transactions will achieve validity consensus, Bob has managed to +"double-spend" his USD to get double the amount of GBP and EUR. We can visualize this as follows: + +.. image:: resources/uniqueness-consensus.png + :scale: 25% + :align: center + +To prevent this, a valid transaction proposal must also achieve uniqueness consensus. Uniqueness consensus is the +requirement that none of the inputs to a proposed transaction have already been consumed in another transaction. + +If one or more of the inputs have already been consumed in another transaction, this is known as a *double spend*, +and the transaction proposal is considered invalid. + +Uniqueness consensus is provided by notaries. See :doc:`key-concepts-notaries` for more details. \ No newline at end of file diff --git a/docs/source/key-concepts-contracts.rst b/docs/source/key-concepts-contracts.rst new file mode 100644 index 0000000000..f4e768fad7 --- /dev/null +++ b/docs/source/key-concepts-contracts.rst @@ -0,0 +1,88 @@ +Contracts +========= + +.. topic:: Summary + + * *A valid transaction must be accepted by the contract of each of its input and output states* + * *Contracts are written in a JVM programming language (e.g. Java or Kotlin)* + * *Contract execution is deterministic and its acceptance of a transaction is based on the transaction's contents alone* + +Video +----- +.. raw:: html + + +

+ +Transaction verification +------------------------ +Recall that a transaction is only valid if it is digitally signed by all required signers. However, even if a +transaction gathers all the required signatures, it is only valid if it is also **contractually valid**. + +**Contract validity** is defined as follows: + +* Each state points to a *contract* +* A *contract* takes a transaction as input, and states whether the transaction is considered valid based on the + contract's rules +* A transaction is only valid if the contract of **every input state** and **every output state** considers it to be + valid + +We can picture this situation as follows: + +.. image:: resources/tx-validation.png + :scale: 25% + :align: center + +The contract code can be written in any JVM language, and has access to the full capabilities of the language, +including: + +* Checking the number of inputs, outputs, commands, timestamps, and/or attachments +* Checking the contents of any of these components +* Looping constructs, variable assignment, function calls, helper methods, etc. +* Grouping similar states to validate them as a group (e.g. imposing a rule on the combined value of all the cash + states) + +A transaction that is not contractually valid is not a valid proposal to update the ledger, and thus can never be +committed to the ledger. In this way, contracts impose rules on the evolution of states over time that are +independent of the willingness of the required signers to sign a given transaction. + +The contract sandbox +-------------------- +Transaction verification must be *deterministic* - a contract should either **always accept** or **always reject** a +given transaction. For example, transaction validity cannot depend on the time at which validation is conducted, or +the amount of information the peer running the contract holds. This is a necessary condition to ensure that all peers +on the network reach consensus regarding the validity of a given ledger update. + +To achieve this, contracts evaluate transactions in a deterministic sandbox. The sandbox has a whitelist that +prevents the contract from importing libraries that could be a source of non-determinism. This includes libraries +that provide the current time, random number generators, libraries that provide filesystem access or networking +libraries, for example. Ultimately, the only information available to the contract when verifying the transaction is +the information included in the transaction itself. + +Contract limitations +-------------------- +Since a contract has no access to information from the outside world, it can only check the transaction for internal +validity. It cannot check, for example, that the transaction is in accordance with what was originally agreed with the +counterparties. + +Peers should therefore check the contents of a transaction before signing it, *even if the transaction is +contractually valid*, to see whether they agree with the proposed ledger update. A peer is under no obligation to +sign a transaction just because it is contractually valid. For example, they may be unwilling to take on a loan that +is too large, or may disagree on the amount of cash offered for an asset. + +Oracles +------- +Sometimes, transaction validity will depend on some external piece of information, such as an exchange rate. In +these cases, an oracle is required. See :doc:`key-concepts-oracles` for further details. + +Legal prose +----------- + +.. raw:: html + + +

+ +Each contract also refers to a legal prose document that states the rules governing the evolution of the state over +time in a way that is compatible with traditional legal systems. This document can be relied upon in the case of +legal disputes. \ No newline at end of file diff --git a/docs/source/key-concepts-core-types.rst b/docs/source/key-concepts-core-types.rst deleted file mode 100644 index d76da267d8..0000000000 --- a/docs/source/key-concepts-core-types.rst +++ /dev/null @@ -1,160 +0,0 @@ -Core types -========== - -Corda provides a large standard library of data types used to represent the :doc:`key-concepts-data-model` previously described. -In addition, there are a series of helper libraries which provide date manipulation, maths and cryptography functions. - -State and References --------------------- -State objects contain mutable data which we would expect to evolve over the lifetime of a contract. - -A reference to a state in the ledger (whether it has been consumed or not) is represented with a ``StateRef`` object. -If the state ref has been looked up from storage, you will have a ``StateAndRef`` which is simply a ``StateRef`` plus the data. - -The ``ContractState`` type is an interface that all states must implement. A ``TransactionState`` is a simple -container for a ``ContractState`` (the custom data used by a contract program) and additional platform-level state -information, such as the *notary* pointer (see :doc:`key-concepts-consensus-notaries`). - -A number of interfaces then extend ``ContractState``, representing standardised functionality for common kinds -of state such as: - - ``OwnableState`` - A state which has an owner (represented as a ``PublicKey`` which can be a ``CompositeKey``, discussed later). Exposes the owner and a function - for replacing the owner e.g. when an asset is sold. - - ``SchedulableState`` - A state to indicate whether there is some activity to be performed at some future point in time with respect to this - contract, what that activity is and at what point in time it should be initiated. - -NamedByHash and UniqueIdentifier --------------------------------- - -Things which are identified by their hash, like transactions and attachments, should implement the ``NamedByHash`` -interface which standardises how the ID is extracted. Note that a hash is *not* a globally unique identifier: it -is always a derivative summary of the contents of the underlying data. Sometimes this isn't what you want: -two deals that have exactly the same parameters and which are made simultaneously but which are logically different -can't be identified by hash because their contents would be identical. Instead you would use ``UniqueIdentifier``. -This is a combination of a (Java) ``UUID`` representing a globally unique 128 bit random number, and an arbitrary -string which can be paired with it. For instance the string may represent an existing "weak" (not guaranteed unique) -identifier for convenience purposes. - - -Transaction lifecycle types ---------------------------- - -A ``WireTransaction`` instance contains the core of a transaction without signatures, and with references to attachments -in place of the attachments themselves (see also :doc:`key-concepts-data-model`). Once signed these are encapsulated in a -``SignedTransaction`` instance. For processing a transaction (i.e. to verify it) a ``SignedTransaction`` is then converted to a -``LedgerTransaction``, which involves verifying the signatures and associating them to the relevant command(s), and -resolving the attachment references to the attachments. Commands with valid signatures are encapsulated in the -``AuthenticatedObject`` type. - -.. note:: A ``LedgerTransaction`` has not necessarily had its contract code executed, and thus could be contract-invalid - (but not signature-invalid). You can use the ``verify`` method as shown below to validate the contracts. - -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 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): - -.. container:: codeset - - .. sourcecode:: kotlin - - val notaryToUse: Party = ... - val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia")) - txb.setTime(Instant.now(), notaryToUse, 30.seconds) - // 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. - txb.toWireTransaction().toLedgerTransaction(services).verify() - -In a unit test, you would typically use a freshly created ``MockServices`` object, or more realistically, you would -write your tests using the :doc:`domain specific language for writing tests `. - -Party and CompositeKey ----------------------- - -Entities using the network are called *parties*. Parties can sign structures using keys, and a party may have many -keys under their control. - -Parties can be represented either in full (including name) or pseudonymously, using the ``Party`` or ``AnonymousParty`` -classes respectively. For example, in a transaction sent to your node as part of a chain of custody it is important you -can convince yourself of the transaction's validity, but equally important that you don't learn anything about who was -involved in that transaction. In these cases ``AnonymousParty`` should be used, which contains a public key (may be a composite key) -without any identifying information about who owns it. In contrast, for internal processing where extended details of -a party are required, the ``Party`` class should be used. The identity service provides functionality for resolving -anonymous parties to full parties. - -An ``AuthenticatedObject`` represents an object (like a command) that has been signed by a set of parties. - -.. note:: These types are provisional and will change significantly in future as the identity framework becomes more fleshed out. - -Multi-signature support ------------------------ - -Corda supports scenarios where more than one key or party is required to authorise a state object transition, for example: -"Either the CEO or 3 out of 5 of his assistants need to provide signatures". - -.. _composite-keys: - -Composite Keys -^^^^^^^^^^^^^^ - -This is achieved by public key composition, using a tree data structure ``CompositeKey``. A ``CompositeKey`` is a tree that -stores the cryptographic public key primitives in its leaves and the composition logic in the intermediary nodes. Every intermediary -node specifies a *threshold* of how many child signatures it requires. - -An illustration of an *"either Alice and Bob, or Charlie"* composite key: - -.. image:: resources/composite-key.png - :align: center - :width: 300px - -To allow further flexibility, each child node can have an associated custom *weight* (the default is 1). The *threshold* -then specifies the minimum total weight of all children required. Our previous example can also be expressed as: - -.. image:: resources/composite-key-2.png - :align: center - :width: 300px - -Verification -^^^^^^^^^^^^ - -Signature verification is performed in two stages: - - 1. Given a list of signatures, each signature is verified against the expected content. - 2. The public keys corresponding to the signatures are matched against the leaves of the composite key tree in question, - and the total combined weight of all children is calculated for every intermediary node. If all thresholds are satisfied, - the composite key requirement is considered to be met. - -Date support ------------- - -There are a number of supporting interfaces and classes for use by contracts which deal with dates (especially in the -context of deadlines). As contract negotiation typically deals with deadlines in terms such as "overnight", "T+3", -etc., it's desirable to allow conversion of these terms to their equivalent deadline. ``Tenor`` models the interval -before a deadline, such as 3 days, etc., while ``DateRollConvention`` describes how deadlines are modified to take -into account bank holidays or other events that modify normal working days. - -Calculating the rollover of a deadline based on working days requires information on the bank holidays involved -(and where a contract's parties are in different countries, for example, this can involve multiple separate sets of -bank holidays). The ``BusinessCalendar`` class models these calendars of business holidays; currently it loads these -from files on disk, but in future this is likely to involve reference data oracles in order to ensure consensus on the -dates used. - -Cryptography and maths support ------------------------------- - -The ``SecureHash`` class represents a secure hash of unknown algorithm. We currently define only a single subclass, -``SecureHash.SHA256``. There are utility methods to create them, parse them and so on. - -We also provide some mathematical utilities, in particular a set of interpolators and classes for working with -splines. These can be found in the `maths package `_. diff --git a/docs/source/key-concepts-data-model.rst b/docs/source/key-concepts-data-model.rst deleted file mode 100644 index bc3d0358ef..0000000000 --- a/docs/source/key-concepts-data-model.rst +++ /dev/null @@ -1,142 +0,0 @@ -Data model -========== - -Overview --------- -Corda uses the so-called "UTXO set" model (unspent transaction output). In this model, the database -does not track accounts or balances. An entry is either spent or not spent but it cannot be changed. In this model the -database is a set of immutable rows keyed by (hash:output index). Transactions define outputs that append new rows and -inputs which consume existing rows. - -The Corda ledger is defined as a set of immutable **states**, which are created and destroyed by digitally signed **transactions**. -Each transaction points to a set of states that it will consume/destroy, these are called **inputs**, and contains a set -of new states that it will create, these are called **outputs**. -Although the ledger is shared, it is not always the case that transactions and ledger entries are globally visible. -In cases where a set of transactions stays within a small subgroup of users it is possible to keep the relevant -data purely within that group. To ensure consistency, we rely heavily on secure hashes like SHA-256 to identify things. - -The Corda model provides the following additional features: - -* There is no global broadcast at any point. -* States can include arbitrary typed data. -* Transactions invoke not only input contracts but also the contracts of the outputs. -* Contracts refer to a bundle of business logic that may handle various different tasks, beyond transaction verification. -* Contracts are Turing-complete and can be written in any ordinary programming language that targets the JVM. -* Arbitrarily-precise time-bounds may be specified in transactions (which must be attested to by a notary) -* Primary consensus implementations use block-free conflict resolution algorithms. -* Transactions are not ordered using a block chain and by implication Corda does not use miners or proof-of-work. - Instead each state points to a notary, which is a service that guarantees it will sign a transaction only if all the - input states are un-consumed. - -Corda provides three main tools to achieve global distributed consensus: - -* Smart contract logic to ensure state transitions are valid according to the pre-agreed rules. -* Uniqueness and timestamping services to order transactions temporally and eliminate conflicts. -* An :doc:`orchestration framework ` which simplifies the process of writing complex multi-step protocols between multiple different parties. - -Comparisons of the Corda data model with Bitcoin and Ethereum can be found in the white papers. - -States ------- -A state object represents an agreement between two or more parties, the evolution of which governed by machine-readable contract code. -This code references, and is intended to implement, portions of human-readable legal prose. -It is intended to be shared only with those who have a legitimate reason to see it. - -The following diagram illustrates a state object: - -.. image:: resources/contract.png - -In the diagram above, we see a state object representing a cash claim of £100 against a commercial bank, owned by a fictional shipping company. - -.. note:: Legal prose (depicted above in grey-shade) is currently implemented as an unparsed reference to the natural language - contract that the code is supposed to express (usually a hash of the contract's contents). - -States contain arbitrary data, but they always contain at minimum a hash of the bytecode of a -**contract code** file, which is a program expressed in JVM byte code that runs sandboxed inside a Java virtual machine. -Contract code (or just "contracts" in the rest of this document) are globally shared pieces of business logic. - -.. note:: In the current code dynamic loading of contracts is not implemented. This will change in the near future. - -Contracts ---------- -Contracts define part of the business logic of the ledger. - -Corda enforces business logic through smart contract code, which is constructed as a pure function (called "verify") that either accepts -or rejects a transaction, and which can be composed from simpler, reusable functions. The functions interpret transactions -as taking states as inputs and producing output states through the application of (smart contract) commands, and accept -the transaction if the proposed actions are valid. Given the same transaction, a contract’s “verify” function always yields -exactly the same result. Contracts do not have storage or the ability to interact with anything. - -.. note:: In the future, contracts will be mobile. Nodes will download and run contracts inside a sandbox without any review in some deployments, - although we envisage the use of signed code for Corda deployments in the regulated sphere. Corda will use an augmented - JVM custom sandbox that is radically more restrictive than the ordinary JVM sandbox, and it will enforce not only - security requirements but also deterministic execution. - -To further aid writing contracts we introduce the concept of :doc:`clauses` which provide a means of re-using common -verification logic. - -Transactions ------------- -Transactions are used to update the ledger by consuming existing state objects and producing new state objects. - -A transaction update is accepted according to the following two aspects of consensus: - - #. Transaction validity: parties can ensure that the proposed transaction and all its ancestors are valid - by checking that the associated contract code runs successfully and has all the required signatures - #. Transaction uniqueness: parties can ensure there exists no other transaction, over which we have previously reached - consensus (validity and uniqueness), that consumes any of the same states. This is the responsibility of a notary service. - -Beyond inputs and outputs, transactions may also contain **commands**, small data packets that -the platform does not interpret itself but which parameterise execution of the contracts. They can be thought of as -arguments to the verify function. Each command has a list of **composite keys** associated with it. The platform ensures -that the transaction has signatures matching every key listed in the commands before the contracts start to execute. Thus, a verify -function can trust that all listed keys have signed the transaction, but is responsible for verifying that any keys required -for the transaction to be valid from the verify function's perspective are included in the list. Public keys -may be random/identityless for privacy, or linked to a well known legal identity, for example via a -*public key infrastructure* (PKI). - -.. note:: Linkage of keys with identities via a PKI is only partially implemented in the current code. - -Commands are always embedded inside a transaction. Sometimes, there's a larger piece of data that can be reused across -many different transactions. For this use case, we have **attachments**. Every transaction can refer to zero or more -attachments by hash. Attachments are always ZIP/JAR files, which may contain arbitrary content. These files are -then exposed on the classpath and so can be opened by contract code in the same manner as any JAR resources -would be loaded. - -Note that there is nothing that explicitly binds together specific inputs, outputs, commands or attachments. Instead, -it's up to the contract code to interpret the pieces inside the transaction and ensure they fit together correctly. This -is done to maximise flexibility for the contract developer. - -Transactions may sometimes need to provide a contract with data from the outside world. Examples may include stock -prices, facts about events or the statuses of legal entities (e.g. bankruptcy), and so on. The providers of such -facts are called **oracles** and they provide facts to the ledger by signing transactions that contain commands they -recognise, or by creating signed attachments. The commands contain the fact and the signature shows agreement to that fact. - -Time is also modelled as a fact and represented as a **timestamping command** placed inside the transaction. This specifies a -time window in which the transaction is considered valid for notarisation. The time window can be open ended (i.e. with a start but no end or vice versa). -In this way transactions can be linked to the notary's clock. - -It is possible for a single Corda network to have multiple competing notaries. A new (output) state is tied to a specific -notary when it is created. Transactions can only consume (input) states that are all associated with the same notary. -A special type of transaction is provided that can move a state (or set of states) from one notary to another. - -.. note:: Currently the platform code will not automatically re-assign states to a single notary. This is a future planned feature. - -Transaction Validation -^^^^^^^^^^^^^^^^^^^^^^ -When a transaction is presented to a node as part of a flow it may need to be checked. Checking original transaction validity is -the responsibility of the ``ResolveTransactions`` flow. This flow performs a breadth-first search over the transaction graph, -downloading any missing transactions into local storage and validating them. The search bottoms out at transactions without inputs -(eg. these are mostly created from issuance transactions). A transaction is not considered valid if any of its transitive dependencies are invalid. - -.. note:: Non-validating notaries assume transaction validity and do not request transaction data or their dependencies - beyond the list of states consumed. - -The tutorial ":doc:`tutorial-contract`" provides a hand-ons walk-through using these concepts. - -Transaction Representation -^^^^^^^^^^^^^^^^^^^^^^^^^^ -By default, all transaction data (input and output states, commands, attachments) is visible to all participants in -a multi-party, multi-flow business workflow. :doc:`merkle-trees` describes how Corda uses Merkle trees to -ensure data integrity and hiding of sensitive data within a transaction that shouldn't be visible in its entirety to all -participants (eg. oracles nodes providing facts). diff --git a/docs/source/key-concepts-ecosystem.rst b/docs/source/key-concepts-ecosystem.rst index 5c4df45442..2e2e900f64 100644 --- a/docs/source/key-concepts-ecosystem.rst +++ b/docs/source/key-concepts-ecosystem.rst @@ -1,47 +1,54 @@ -Corda ecosystem -=============== +The network +=========== -A Corda network consists of the following components: +.. topic:: Summary -* Nodes, where each node represents a JVM run-time environment hosting Corda services and executing applications ("CorDapps"). - Nodes communicate using AMQP/1.0 over TLS. -* A permissioning service that automates the process of provisioning TLS certificates. -* A network map service that publishes information about nodes on the network. -* One or more pluggable notary service types (which may be distributed over multiple nodes). - A notary guarantees uniqueness and validity of transactions. -* Zero or more oracle services. An oracle is a well known service that signs transactions if they state a fact and that fact is considered to be true. -* CorDapps which represent participant applications that execute contract code and communicate using the flow framework to achieve consensus over some business activity -* Standalone Corda applications that provide manageability and tooling support to a Corda network. + * *A Corda network is made up of nodes running Corda and CorDapps* + * *The network is permissioned, with access controlled by a doorman* + * *Communication between nodes is point-to-point, instead of relying on global broadcasts* + +Network structure +----------------- +A Corda network is an authenticated peer-to-peer network of nodes, where each node is a JVM run-time environment +hosting Corda services and executing applications known as *CorDapps*. + +All communication between nodes is direct, with TLS-encrypted messages sent over AMQP/1.0. This means that data is +shared only on a need-to-know basis; in Corda, there are **no global broadcasts**. + +Each network has a **network map service** that publishes the IP addresses through which every node on the network can +be reached, along with the identity certificates of those nodes and the services they provide. + +The doorman +----------- +Corda networks are semi-private. Each network has a doorman service that enforces rules regarding the information +that nodes must provide and the know-your-customer processes that they must complete before being admitted to the +network. + +To join the network, a node must contact the doorman and provide the required information. If the doorman is +satisfied, the node will receive a root-authority-signed TLS certificate from the network's permissioning service. +This certificate certifies the node's identity when communicating with other participants on the network. + +We can visualize a network as follows: + +.. image:: resources/network.png + :scale: 25% + :align: center + +Network services +---------------- +Nodes can provide several types of services: + +* One or more pluggable **notary services**. Notaries guarantee the uniqueness, and possibility the validity, of ledger + updates. Each notary service may be run on a single node, or across a cluster of nodes. +* Zero or more **oracle services**. An oracle is a well-known service that signs transactions if they state a fact and + that fact is considered to be true. These components are illustrated in the following diagram: .. image:: resources/cordaNetwork.png - :align: center + :scale: 25% + :align: center -Note: - -* Corda infrastructure services are those which all participants depend upon, such as the network map and notaries. -* Corda services can be deployed by participants, third parties or a central network operator (eg. such as R3); - this diagram is not intended to imply only a centralised model is supported - -It is important to highlight the following: - -* Corda is designed for semi-private networks in which admission requires obtaining an identity signed by a root authority. -* Nodes are arranged in an authenticated peer to peer network. All communication is direct. -* 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. -* Nodes are backed by a relational database and data placed in the ledger can be queried using SQL -* The network map publishes the IP addresses through which every node on the network can be reached, along with the identity certificates of those nodes and the services they provide. -* All communication takes the form of small multi-party sub-protocols called flows. -* Oracles represent gateways to proprietary (or other) business logic executors (e.g., central counterparties or valuation agents) that can be verified on-ledger by participants. - -CorDapps --------- -Corda is a platform for the writing of “CorDapps”: applications that extend the distributed ledger with new capabilities. -Such apps define new data types, new inter-node protocol flows and the “smart contracts” that determine allowed changes. -The combination of state objects (data), contract code (allowable operations), transaction flows (business logic -choreography), any necessary APIs, vault plugins, and UI components can be thought of as a shared ledger application, -or corda distributed application (“CorDapp”). This is the core set of components a contract developer on the platform -should expect to build. - -Examples of CorDapps include asset trading (see :ref:`irs-demo` and :ref:`trader-demo`), portfolio valuations (see :ref:`simm-demo`), trade finance, -post-trade order matching, KYC/AML, etc. \ No newline at end of file +In this diagram, Corda infrastructure services are those upon which all participants depend, such as the network map +and notary services. Corda services may be deployed by participants, third parties or a central network operator +(such as R3). The diagram is not intended to imply that only a centralised model is supported. \ No newline at end of file diff --git a/docs/source/key-concepts-flow-framework.rst b/docs/source/key-concepts-flow-framework.rst deleted file mode 100644 index 81d744e7d3..0000000000 --- a/docs/source/key-concepts-flow-framework.rst +++ /dev/null @@ -1,37 +0,0 @@ - -Flow framework --------------- -In Corda all communication takes the form of structured sequences of messages passed between parties which we call flows. - -Flows enable complex multi-step, multi-party business interactions to be modelled as blocking code without a central controller. -The code is transformed into an asynchronous state machine, with checkpoints written to the node’s backing database when messages are sent and received. -A node may potentially have millions of flows active at once and they may last days, across node restarts and even upgrades. - -A flow library is provided to enable developers to re-use common flow types such as notarisation, membership broadcast, -transaction resolution and recording, and so on. - -APIs are provided to send and receive object graphs to and from other identities on the network, embed sub-flows, -report progress information to observers and even interact with people (for manual resolution of exceptional scenarios) - -Flows are embedded within CorDapps and deployed to a participant's node for execution. - -.. note:: We will be implementing the concept of a flow hospital to provide a means for a node administrator to decide - whether a paused flow should be killed or repaired. Flows enter this state if they throw exceptions or explicitly request human assistance. - -Section 4 of the `Technical white paper`_ provides further detail of the above features. - -The following diagram illustrates a sample multi-party business flow: - -.. image:: resources/flowFramework.png - -Note the following: - -* there are 3 participants in this workflow including the notary -* the Buyer and Seller flows (depicted in green) are custom written by developers and deployed within a CorDapp -* the custom written flows invoke both financial library flows such as ``TwoPartyTradeFlow`` (depicted in orange) and core - library flows such as ``ResolveTransactionsFlow`` and ``NotaryFlow`` (depicted in yellow) -* each side of the flow illustrates the stage of execution with a progress tracker notification -* activities within a flow directly or indirectly interact with its node's ledger (eg. to record a signed, notarised transaction) and vault (eg. to perform a spend of some fungible asset) -* flows interact across parties using send, receive and sendReceive messaging semantics (by implementing the ``FlowLogic`` interface) - -.. _`Technical white paper`: _static/corda-technical-whitepaper.pdf \ No newline at end of file diff --git a/docs/source/key-concepts-flows.rst b/docs/source/key-concepts-flows.rst new file mode 100644 index 0000000000..1452efe2e8 --- /dev/null +++ b/docs/source/key-concepts-flows.rst @@ -0,0 +1,87 @@ +Flows +===== + +.. topic:: Summary + + * *Flows automate the process of agreeing ledger updates* + * *Communication between nodes only occurs in the context of these flows, and is point-to-point* + * *Built-in flows are provided to automate common tasks* + +Video +----- +.. raw:: html + + +

+ +Motivation +---------- +Corda networks use point-to-point messaging instead of a global broadcast. This means that coordinating a ledger update +requires network participants to specify exactly what information needs to be sent, to which counterparties, and in +what order. + +Here is a visualisation of the process of agreeing a simple ledger update between Alice and Bob: + +.. image:: resources/flow.gif + :scale: 25% + :align: center + +The flow framework +------------------ +Rather than having to specify these steps manually, Corda automates the process using *flows*. A flow is a sequence +of steps that tells a node how to achieve a specific ledger update, such as issuing an asset or settling a trade. + +Here is the sequence of flow steps involved in the simple ledger update above: + +.. image:: resources/flow-sequence.png + :scale: 25% + :align: center + +Running flows +------------- +Once a given business process has been encapsulated in a flow and installed on the node as part of a CorDapp, the node's +owner can instruct the node to kick off this business process at any time using an RPC call. The flow abstracts all +the networking, I/O and concurrency issues away from the node owner. + +All activity on the node occurs in the context of these flows. Unlike contracts, flows do not execute in a sandbox, +meaning that nodes can perform actions such as networking, I/O and use sources of randomness within the execution of a +flow. + +Inter-node communication +^^^^^^^^^^^^^^^^^^^^^^^^ +Nodes communicate by passing messages between flows. Each node has zero or more flow classes that are registered to +respond to messages from a single other flow. + +Suppose Alice is a node on the network and wishes to agree a ledger update with Bob, another network node. To +communicate with Bob, Alice must: + +* Start a flow that Bob is registered to respond to +* Send Bob a message within the context of that flow +* Bob will start its registered counterparty flow + +Now that a connection is established, Alice and Bob can communicate to agree a ledger update by passing a series of +messages back and forth, as prescribed by the flow steps. + +Subflows +^^^^^^^^ +Flows can be composed by starting a flow as a subprocess in the context of another flow. The flow that is started as +a subprocess is known as a *subflow*. The parent flow will wait until the subflow returns. + +The flow library +~~~~~~~~~~~~~~~~ +Corda provides a library of flows to handle common tasks, meaning that developers do not have to redefine the +logic behind common processes such as: + +* Notarising and recording a transaction +* Gathering signatures from counterparty nodes +* Verifying a chain of transactions + +Further information on the available built-in flows can be found in :doc:`flow-library`. + +Concurrency +----------- +The flow framework allows nodes to have many flows active at once. These flows may last days, across node restarts and even upgrades. + +This is achieved by serializing flows to disk whenever they enter a blocking state (e.g. when they're waiting on I/O +or a networking call). Instead of waiting for the flow to become unblocked, the node immediately starts work on any +other scheduled flows, only returning to the original flow at a later date. diff --git a/docs/source/key-concepts-ledger.rst b/docs/source/key-concepts-ledger.rst new file mode 100644 index 0000000000..8741c8da82 --- /dev/null +++ b/docs/source/key-concepts-ledger.rst @@ -0,0 +1,37 @@ +The ledger +========== + +.. topic:: Summary + + * *The ledger is subjective from each peer's perspective* + * *Two peers are always guaranteed to see the exact same version of any on-ledger facts they share* + +Video +----- +.. raw:: html + + +

+ +Overview +-------- +In Corda, there is **no single central store of data**. Instead, each node maintains a separate database of known +facts. As a result, each peer only sees a subset of facts on the ledger, and no peer is aware of the ledger in its +entirety. + +For example, imagine a network with five nodes, where each coloured circle represents a shared fact: + +.. image:: resources/ledger-venn.png + :scale: 25% + :align: center + +We can see that although Carl, Demi and Ed are aware of shared fact 3, **Alice and Bob are not**. + +Equally importantly, Corda guarantees that whenever one of these facts is shared by multiple nodes on the network, it evolves +in lockstep in the database of every node that is aware of it: + +.. image:: resources/ledger-table.png + :scale: 25% + :align: center + +For example, Alice and Bob will both see the **exact same version** of shared facts 1 and 7. \ No newline at end of file diff --git a/docs/source/key-concepts-node.rst b/docs/source/key-concepts-node.rst new file mode 100644 index 0000000000..aa6517249a --- /dev/null +++ b/docs/source/key-concepts-node.rst @@ -0,0 +1,79 @@ +Nodes +===== + +.. topic:: Summary + + * *A node is JVM run-time with a unique network identity running the Corda software* + * *The node has two interfaces with the outside world:* + + * *A network layer, for interacting with other nodes* + * *RPC, for interacting with the node's owner* + + * *The node's functionality is extended by installing CorDapps in the plugin registry* + +Video +----- +.. raw:: html + +

Corda Node, CorDapps and Network

+ +

+ +Node architecture +----------------- +A Corda node is a JVM run-time environment with a unique identity on the network that hosts Corda services and +CorDapps. + +We can visualize the node's internal architecture as follows: + +.. image:: resources/node-architecture.png + :scale: 25% + :align: center + +The core elements of the architecture are: + +* A persistence layer for storing data +* A network interface for interacting with other nodes +* An RPC interface for interacting with the node's owner +* A service hub for allowing the node's flows to call upon the node's other services +* A plugin registry for extending the node by installing CorDapps + +Persistence layer +----------------- +The persistence layer has two parts: + +* The **vault**, where the node stores any relevant current and historic states +* The **storage service**, where it stores transactions, attachments and flow checkpoints + +The node's owner can query the node's storage using the RPC interface (see below). + +Network interface +----------------- +All communication with other nodes on the network is handled by the node itself, as part of running a flow. The +node's owner does not interact with other network nodes directly. + +RPC interface +------------- +The node's owner interacts with the node via remote procedure calls (RPC). The key RPC operations the node exposes +are documented in :doc:``api-rpc``. + +The service hub +--------------- +Internally, the node has access to a rich set of services that are used during flow execution to coordinate ledger +updates. The key services provided are: + +* Information on other nodes on the network and the services they offer +* Access to the contents of the vault and the storage service +* Access to, and generation of, the node's public-private keypairs +* Information about the node itself +* The current time, as tracked by the node + +The plugin registry +------------------- +The plugin registry is where new CorDapps are installed to extend the behavior of the node. + +The node also has several plugins installed by default to handle common tasks such as: + +* Retrieving transactions and attachments from counterparties +* Upgrading contracts +* Broadcasting agreed ledger updates for recording by counterparties \ No newline at end of file diff --git a/docs/source/key-concepts-notaries.rst b/docs/source/key-concepts-notaries.rst new file mode 100644 index 0000000000..5408fae4b6 --- /dev/null +++ b/docs/source/key-concepts-notaries.rst @@ -0,0 +1,95 @@ +Notaries +======== + +.. topic:: Summary + + * *Notaries prevent "double-spends"* + * *Notaries may optionally also validate transactions* + * *A network can have several notaries, each running a different consensus algorithm* + +Video +----- +.. raw:: html + + +

+ +Overview +-------- +A *notary* is a network service that provides **uniqueness consensus** by attesting that, for a given transaction, it +has not already signed other transactions that consumes any of the proposed transaction's input states. + +Upon being sent asked to notarise a transaction, a notary will either: + +* Sign the transaction if it has not already signed other transactions consuming any of the proposed transaction's + input states +* Reject the transaction and flag that a double-spend attempt has occurred otherwise + +In doing so, the notary provides the point of finality in the system. Until the notary's signature is obtained, parties +cannot be sure that an equally valid, but conflicting, transaction will not be regarded as the "valid" attempt to spend +a given input state. However, after the notary's signature is obtained, we can be sure that the proposed +transaction's input states had not already been consumed by a prior transaction. Hence, notarisation is the point +of finality in the system. + +Every state has an appointed notary, and a notary will only notarise a transaction if it is the appointed notary +of all the transaction's input states. + +Consensus algorithms +-------------------- +Corda has "pluggable" consensus, allowing notaries to choose a consensus algorithm based on their requirements in +terms of privacy, scalability, legal-system compatibility and algorithmic agility. + +In particular, notaries may differ in terms of: + +* **Structure** - a notary may be a single network node, a cluster of mutually-trusting nodes, or a cluster of + mutually-distrusting nodes +* **Consensus algorithm** - a notary service may choose to run a high-speed, high-trust algorithm such as RAFT, a + low-speed, low-trust algorithm such as BFT, or any other consensus algorithm it chooses + +Validation +^^^^^^^^^^ +A notary service must also decide whether or not to provide **validity consensus** by validating each transaction +before committing it. In making this decision, they face the following trade-off: + +* If a transaction **is not** checked for validity, it creates the risk of "denial of state" attacks, where a node + knowingly builds an invalid transaction consuming some set of existing states and sends it to the + notary, causing the states to be marked as consumed + +* If the transaction **is** checked for validity, the notary will need to see the full contents of the transaction and + its dependencies. This leaks potentially private data to the notary + +There are several further points to keep in mind when evaluating this trade-off. In the case of the non-validating +model, Corda's controlled data distribution model means that information on unconsumed states is not widely shared. +Additionally, Corda's permissioned network means that the notary can store to the identity of the party that created +the "denial of state" transaction, allowing the attack to be resolved off-ledger. + +In the case of the validating model, the use of anonymous, freshly-generated public keys instead of legal identities to +identify parties in a transaction limit the information the notary sees. + +Multiple notaries +----------------- +Each Corda network can have multiple notaries, each potentially running a different consensus algorithm. This provides +several benefits: + +* **Privacy** - we can have both validating and non-validating notary services on the same network, each running a + different algorithm. This allows nodes to choose the preferred notary on a per-transaction basis +* **Load balancing** - spreading the transaction load over multiple notaries allows higher transaction throughput for + the platform overall +* **Low latency** - latency can be minimised by choosing a notary physically closer to the transacting parties + +Changing notaries +^^^^^^^^^^^^^^^^^ +Remember that a notary will only sign a transaction if it is the appointed notary of all of the transaction's input +states. However, there are cases in which we may need to change a state's appointed notary. These include: + +* When a single transaction needs to consume several states that have different appointed notaries +* When a node would prefer to use a different notary for a given transaction due to privacy or efficiency concerns + +Before these transactions can be created, the states must first be repointed to all have the same notary. This is +achieved using a special notary-change transaction that takes: + +* A single input state +* An output state identical to the input state, except that the appointed notary has been changed + +The input state's appointed notary will sign the transaction if it doesn't constitute a double-spend, at which point +a state will enter existence that has all the properties of the old state, but has a different appointed notary. \ No newline at end of file diff --git a/docs/source/key-concepts-oracles.rst b/docs/source/key-concepts-oracles.rst new file mode 100644 index 0000000000..9dca75a82f --- /dev/null +++ b/docs/source/key-concepts-oracles.rst @@ -0,0 +1,79 @@ +Oracles +======= + +.. topic:: Summary + + * *A fact can be included in a transaction as part of a command* + * *An oracle is a service that will only sign the transaction if the included fact is true* + +Video +----- +.. raw:: html + + +

+ +Overview +-------- +In many cases, a transaction's contractual validity depends on some external piece of data, such as the current +exchange rate. However, if we were to let each participant evaluate the transaction's validity based on their own +view of the current exchange rate, the contract's execution would be non-deterministic: some signers would consider the +transaction valid, while others would consider it invalid. As a result, disagreements would arise over the true state +of the ledger. + +Corda addresses this issue using *oracles*. Oracles are network services that, upon request, provide commands +that encapsulate a specific fact (e.g. the exchange rate at time x) and list the oracle as a required signer. + +If a node wishes to use a given fact in a transaction, they request a command asserting this fact from the oracle. If +the oracle considers the fact to be true, they send back the required command. The node then includes the command in +their transaction, and the oracle will sign the transaction to assert that the fact is true. + +If they wish to monetize their services, oracles can choose to only sign a transaction and attest to the validity of +the fact it contains for a fee. + +Transaction tear-offs +--------------------- +To sign a transaction, the only information the oracle needs to see is their embedded command. Providing any +additional transaction data to the oracle would constitute a privacy leak. Similarly, a non-validating notary only +needs to see a transaction's input states. + +To combat this, the transaction proposer(s) uses a Merkle tree to "tear off" any parts of the transaction that the +oracle/notary doesn't need to see before presenting it to them for signing. A Merkle tree is a well-known cryptographic +scheme that is commonly used to provide proofs of inclusion and data integrity. Merkle trees are widely used in +peer-to-peer networks, blockchain systems and git. + +The advantage of a Merkle tree is that the parts of the transaction that were torn off when presenting the transaction +to the oracle cannot later be changed without also invalidating the oracle's digital signature. + +Transaction Merkle trees +^^^^^^^^^^^^^^^^^^^^^^^^ +A Merkle tree is constructed from a transaction by splitting the transaction into leaves, where each leaf contains +either an input, an output, a command, or an attachment. The Merkle tree also contains the other fields of the +``WireTransaction``, such as the timestamp, the notary, the type and the signers. + +Next, the Merkle tree is built in the normal way by hashing the concatenation of nodes’ hashes below the current one +together. It’s visible on the example image below, where ``H`` denotes sha256 function, "+" - concatenation. + +.. image:: resources/merkleTree.png + +The transaction has two input states, one output state, one attachment, one command and a timestamp. For brevity +we didn't include all leaves on the diagram (type, notary and signers are presented as one leaf labelled Rest - in +reality they are separate leaves). Notice that if a tree is not a full binary tree, leaves are padded to the nearest +power of 2 with zero hash (since finding a pre-image of sha256(x) == 0 is hard computational task) - marked light +green above. Finally, the hash of the root is the identifier of the transaction, it's also used for signing and +verification of data integrity. Every change in transaction on a leaf level will change its identifier. + +Hiding data +^^^^^^^^^^^ +Hiding data and providing the proof that it formed a part of a transaction is done by constructing Partial Merkle Trees +(or Merkle branches). A Merkle branch is a set of hashes, that given the leaves’ data, is used to calculate the +root’s hash. Then that hash is compared with the hash of a whole transaction and if they match it means that data we +obtained belongs to that particular transaction. + +.. image:: resources/partialMerkle.png + +In the example above, the node ``H(f)`` is the one holding command data for signing by Oracle service. Blue leaf +``H(g)`` is also included since it's holding timestamp information. Nodes labelled ``Provided`` form the Partial +Merkle Tree, black ones are omitted. Having timestamp with the command that should be in a violet node place and +branch we are able to calculate root of this tree and compare it with original transaction identifier - we have a +proof that this command and timestamp belong to this transaction. \ No newline at end of file diff --git a/docs/source/key-concepts-security-model.rst b/docs/source/key-concepts-security-model.rst deleted file mode 100644 index 66be831da0..0000000000 --- a/docs/source/key-concepts-security-model.rst +++ /dev/null @@ -1,45 +0,0 @@ -Security model -============== - -Corda has been designed from the ground up to implement a global, decentralised database where all nodes are assumed to be -untrustworthy. This means that each node must actively cross-check each other's work to reach consensus -amongst a group of interacting participants. - -The security model plays a role in the following areas: - -* Identity: - Corda is designed for semi-private networks in which admission requires obtaining an identity signed by a root authority. - This assumption is pervasive – the flow API provides messaging in terms of identities, with routing and delivery to underlying nodes being handled automatically. - See sections 3.2 of the `Technical white paper`_ for further details on identity and the permissioning service. - -* Notarisation: pluggable notaries and algorithms offering different levels of trust. - Notaries may be validating or non-validating. A validating notary will resolve and fully check transactions they are asked to deconflict. - Without the use of any other privacy features, they gain full visibility into every transaction. - On the other hand, non-validating notaries assume transaction validity and do not request transaction data or their dependencies - beyond the list of states consumed (and thus, their level of trust is much lower and exposed to malicious use of transaction inputs). - From an algorithm perspective, Corda currently provides a distributed notary implementation that uses Raft. - -.. note:: A byzantine fault tolerant notary based on the BFT-SMaRT algorithm is included in the code, but is - still incubating and is not yet ready for use. - -* Authentication, authorisation and entitlements: - Network permissioning, including node to node authentication, is performed using TLS and certificates. - See :doc:`permissioning` for further detail. - -.. warning:: API level authentication (RPC, Web) is currently simple username/password for demonstration purposes and will be revised. - Similarly, authorisation is currently based on permission groups applied to flow execution. - -Privacy techniques - -* Partial data visibility: transactions are not globally broadcast as in many other systems. -* Transaction tear-offs: Transactions are structured as Merkle trees, and may have individual subcomponents be revealed to parties who already know the Merkle root hash. Additionally, they may sign the transaction without being able to see all of it. - - See :doc:`merkle-trees` for further detail. - -* Multi-signature support: Corda uses composite keys to support scenarios where more than one key or party is required to authorise a state object transition. - -.. note:: Future privacy techniques will include key randomisation, graph pruning, deterministic JVM sandboxing and support for secure signing devices. - See sections 10 and 13 of the `Technical white paper`_ for detailed descriptions of these techniques and features. - -.. _`Technical white paper`: _static/corda-technical-whitepaper.pdf - diff --git a/docs/source/key-concepts-states.rst b/docs/source/key-concepts-states.rst new file mode 100644 index 0000000000..f4bf72be94 --- /dev/null +++ b/docs/source/key-concepts-states.rst @@ -0,0 +1,59 @@ +States +====== + +.. topic:: Summary + + * *States represent on-ledger facts* + * *States are evolved by marking the current state as historic and creating an updated state* + * *Each node has a vault where it stores any relevant states to itself* + +Video +----- +.. raw:: html + + +

+ +Overview +-------- +A *state* is an immutable object representing a fact known by one or more Corda nodes at a specific point in time. +States can contain arbitrary data, allowing them to represent facts of any kind (e.g. stocks, bonds, loans, KYC data, +identity information...). + +For example, the following state represents an IOU - an agreement that Alice owes Bob an amount X: + +.. image:: resources/state.png + :scale: 25% + :align: center + +Specifically, this state represents an IOU of £10 from Alice to Bob. + +As well as any information about the fact itself, the state also contains a reference to the *contract* that governs +the evolution of the state over time. We discuss contracts in :doc:`key-concepts-contracts`. + +State sequences +--------------- +As states are immutable, they cannot be modified directly to reflect a change in the state of the world. + +Instead, the lifecycle of a shared fact over time is represented by a **state sequence**. When a state needs to be +updated, we create a new version of the state representing the new state of the world, and mark the existing state as +historic. + +This sequence of state replacements gives us a full view of the evolution of the shared fact over time. We can +picture this situation as follows: + +.. image:: resources/state-sequence.png + :scale: 25% + :align: center + +The vault +--------- +Each node on the network maintains a *vault* - a database where it tracks all the current and historic states that it +is aware of, and which it considers to be relevant to itself: + +.. image:: resources/vault-simple.png + :scale: 25% + :align: center + +We can think of the ledger from each node's point of view as the set of all the current (i.e. non-historic) states that +it is aware of. \ No newline at end of file diff --git a/docs/source/key-concepts-time-windows.rst b/docs/source/key-concepts-time-windows.rst new file mode 100644 index 0000000000..c6002fa710 --- /dev/null +++ b/docs/source/key-concepts-time-windows.rst @@ -0,0 +1,59 @@ +Time-windows +============ + +.. topic:: Summary + + * *If a transaction includes a time-window, it can only be committed during that window* + * *The notary is the timestamping authority, refusing to commit transactions outside of that window* + * *Time-windows can have a start and end time, or be open at either end* + +Video +----- +.. raw:: html + + +

+ +Time in a distributed system +---------------------------- +A notary also act as the *timestamping authority*, verifying that a transaction occurred during a specific time-window +before notarising it. + +For a time-window to be meaningful, its implications must be binding on the party requesting it. A party can obtain a +time-window signature in order to prove that some event happened *before*, *on*, or *after* a particular point in time. +However, if the party is not also compelled to commit to the associated transaction, it has a choice of whether or not +to reveal this fact until some point in the future. As a result, we need to ensure that the notary either has to also +sign the transaction within some time tolerance, or perform timestamping *and* notarisation at the same time. The +latter is the chosen behaviour for this model. + +There will never be exact clock synchronisation between the party creating the transaction and the notary. +This is not only due to issues of physics and network latency, but also because between inserting the command and +getting the notary to sign there may be many other steps (e.g. sending the transaction to other parties involved in the +trade, requesting human sign-off...). Thus the time at which the transaction is sent for notarisation may be quite +different to the time at which the transaction was created. + +Time-windows +------------ +For this reason, times in transactions are specified as time *windows*, not absolute times. In a distributed system +there can never be "true time", only an approximation of it. Time windows can be open-ended (i.e. specify only one of +"before" and "after") or they can be fully bounded. + +.. image:: resources/time-window.gif + :scale: 25% + :align: center + +In this way, we express the idea that the *true value* of the fact "the current time" is actually unknowable. Even when +both a before and an after time are included, the transaction could have occurred at any point within that time-window. + +By creating a range that can be either closed or open at one end, we allow all of the following situations to be +modelled: + +* A transaction occurring at some point after the given time (e.g. after a maturity event) +* A transaction occurring at any time before the given time (e.g. before a bankruptcy event) +* A transaction occurring at some point roughly around the given time (e.g. on a specific day) + +If a time window needs to be converted to an absolute time (e.g. for display purposes), there is a utility method to +calculate the mid point. + +.. note:: It is assumed that the time feed for a notary is GPS/NaviStar time as defined by the atomic + clocks at the US Naval Observatory. This time feed is extremely accurate and available globally for free. diff --git a/docs/source/key-concepts-tradeoffs.rst b/docs/source/key-concepts-tradeoffs.rst new file mode 100644 index 0000000000..c1e68a6696 --- /dev/null +++ b/docs/source/key-concepts-tradeoffs.rst @@ -0,0 +1,76 @@ +Tradeoffs +========= + +.. topic:: Summary + + * *Permissioned networks are better suited for financial use-cases* + * *Point-to-point communication allows information to be shared need-to-know* + * *A UTXO model allows for more transactions-per-second* + +Permissioned vs. permissionless +------------------------------- +Traditional blockchain networks are *permissionless*. The parties on the network are anonymous, and can join and +leave at will. + +By contrast, Corda networks are *permissioned*. Each party on the network has a known identity that they use when +communicating with counterparties, and network access is controlled by a doorman. This has several benefits: + +* Anonymous parties are inappropriate for most scenarios involving regulated financial institutions +* Knowing the identity of your counterparties allows for off-ledger resolution of conflicts using existing + legal systems +* Sybil attacks are averted without the use of expensive mechanisms such as proof-of-work + +Point-to-point vs. global broadcasts +------------------------------------ +Traditional blockchain networks broadcast every message to every participant. The reason for this is two-fold: + +* Counterparty identities are not known, so a message must be sent to every participant to ensure it reaches its + intended recipient +* Making every participant aware of every transaction allows the network to prevent double-spends + +The downside is that all participants see everyone else's data. This is unacceptable for many use-cases. + +In Corda, each message is instead addressed to a specific counterparty, and is not seen by any uninvolved third +parties. The developer has full control over what messages are sent, to whom, and in what order. As a result, **data +is shared on a need-to-know basis only**. To prevent double-spends in this system, we employ notaries as +an alternative to proof-of-work. + +Corda also uses several other techniques to maximize privacy on the network: + +* **Transaction tear-offs**: Transactions are structured in a way that allows them to be digitally signed without + disclosing the transaction's contents. This is achieved using a data structure called a Merkle tree. You can read + more about this technique in :doc:`merkle-trees`. +* **Key randomisation**: The parties to a transaction are identified only by their public keys, and fresh keypairs are + generated for each transaction. As a result, an onlooker cannot identify which parties were involved in a given + transaction. + +UTXO vs. account model +---------------------- +Corda uses a *UTXO* (unspent transaction output) model. Each transaction consumes a set of existing states to produce +a set of new states. + +The alternative would be an *account* model. In an account model, stateful objects are stored on-ledger, and +transactions take the form of requests to update the current state of these objects. + +The main advantage of the UTXO model is that transactions with different inputs can be applied in parallel, +vastly increasing the network's potential transactions-per-second. In the account model, the number of +transactions-per-second is limited by the fact that updates to a given object must be applied sequentially. + +Code-is-law vs. existing legal systems +-------------------------------------- +Financial institutions need the ability to resolve conflicts using the traditional legal system where required. Corda +is designed to make this possible by: + +* Having permissioned networks, meaning that participants are aware of who they are dealing with in every single + transaction +* All code contracts are backed by a legal document describing the contract's intended behavior which can be relied + upon to resolve conflicts + +Build vs. re-use +---------------- +Wherever possible, Corda re-uses existing technologies to make the platform more robust platform overall. For +example, Corda re-uses: + +* Standard JVM programming languages for the development of CorDapps +* Existing SQL databases +* Existing message queue implementations \ No newline at end of file diff --git a/docs/source/key-concepts-transactions.rst b/docs/source/key-concepts-transactions.rst new file mode 100644 index 0000000000..1e2c1ff836 --- /dev/null +++ b/docs/source/key-concepts-transactions.rst @@ -0,0 +1,181 @@ +Transactions +============ + +.. topic:: Summary + + * *Transactions are proposals to update the ledger* + * *A transaction proposal will only be committed if:* + + * *It doesn't contain double-spends* + * *It is contractually valid* + * *It is signed by the required parties* + +Video +----- +.. raw:: html + + +

+ +Overview +-------- +Corda uses a *UTXO* (unspent transaction output) model where every state on the ledger is immutable. The ledger +evolves over time by applying *transactions*, which update the ledger by marking zero or more existing ledger states +as historic (the *inputs*) and producing zero or more new ledger states (the *outputs*). Transactions represent a +single link in the state sequences seen in :doc:`key-concepts-states`. + +Here is an example of an update transaction, with two inputs and two outputs: + +.. image:: resources/basic-tx.png + :scale: 25% + :align: center + +A transaction can contain any number of inputs and outputs of any type: + +* They can include many different state types (e.g. both cash and bonds) +* They can be issuances (have zero inputs) or exits (have zero outputs) +* They can merge or split fungible assets (e.g. combining a $2 state and a $5 state into a $7 cash state) + +Transactions are *atomic*: either all the transaction's proposed changes are accepted, or none are. + +There are two basic types of transactions: + +* Notary-change transactions (used to change a state's notary - see :doc:`key-concepts-notaries`) +* General transactions (used for everything else) + +Transaction chains +------------------ +When creating a new transaction, the output states that the transaction will propose do not exist yet, and must +therefore be created by the proposer(s) of the transaction. However, the input states already exist as the outputs of +previous transactions. We therefore include them in the proposed transaction by reference. + +These input states references are a combination of: + +* The hash of the transaction that created the input +* The input's index in the outputs of the previous transaction + +This situation can be illustrated as follows: + +.. image:: resources/tx-chain.png + :scale: 25% + :align: center + +These input state references link together transactions over time, forming what is known as a *transaction chain*. + +Committing transactions +----------------------- +Initially, a transaction is just a **proposal** to update the ledger. It represents the future state of the ledger +that is desired by the transaction builder(s): + +.. image:: resources/uncommitted_tx.png + :scale: 25% + :align: center + +To become reality, the transaction must receive signatures from all of the *required signers* (see **Commands**, below). Each +required signer appends their signature to the transaction to indicate that they approve the proposal: + +.. image:: resources/tx_with_sigs.png + :scale: 25% + :align: center + +If all of the required signatures are gathered, the transaction becomes committed: + +.. image:: resources/committed_tx.png + :scale: 25% + :align: center + +This means that: + +* The transaction's inputs are marked as historic, and cannot be used in any future transactions +* The transaction's outputs become part of the current state of the ledger + +Transaction validity +-------------------- +Each required signers should only sign the transaction if the following two conditions hold: + + * **Transaction validity**: For both the proposed transaction, and every transaction in the chain of transactions + that created the current proposed transaction's inputs: + * The transaction is digitally signed by all the required parties + * The transaction is *contractually valid* (see :doc:`key-concepts-contracts`) + * **Transaction uniqueness**: There exists no other committed transaction that has consumed any of the inputs to + our proposed transaction (see :doc:`key-concepts-consensus`) + +If the transaction gathers all the required signatures but these conditions do not hold, the transaction's outputs +will not be valid, and will not be accepted as inputs to subsequent transactions. + +Other transaction components +---------------------------- +As well as input states and output states, transactions may contain: + +* Commands +* Attachments +* Timestamps + +For example, a transaction where Alice pays off £5 of an IOU with Bob using a £5 cash payment, supported by two +attachments and a timestamp, may look as follows: + +.. image:: resources/full-tx.png + :scale: 25% + :align: center + +We explore the role played by the remaining transaction components below. + +Commands +^^^^^^^^ +.. raw:: html + + +

+ +Suppose we have a transaction with a cash state and a bond state as inputs, and a cash state and a bond state as +outputs. This transaction could represent two different scenarios: + +* A bond purchase +* A coupon payment on a bond + +We can imagine that we'd want to impose different rules on what constitutes a valid transaction depending on whether +this is a purchase or a coupon payment. For example, in the case of a purchase, we would require a change in the bond's +current owner, whereas in the case of a coupon payment, we would require that the ownership of the bond does not +change. + +For this, we have *commands*. Including a command in a transaction allows us to indicate the transaction's intent, +affecting how we check the validity of the transaction. + +Each command is also associated with a list of one or more *signers*. By taking the union of all the public keys +listed in the commands, we get the list of the transaction's required signers. In our example, we might imagine that: + +* In a coupon payment on a bond, only the owner of the bond is required to sign +* In a cash payment, only the owner of the cash is required to sign + +We can visualize this situation as follows: + +.. image:: resources/commands.png + :scale: 25% + :align: center + +Attachments +^^^^^^^^^^^ +.. raw:: html + + +

+ +Sometimes, we have a large piece of data that can be reused across many different transactions. Some examples: + +* A calendar of public holidays +* Supporting legal documentation +* A table of currency codes + +For this use case, we have *attachments*. Each transaction can refer to zero or more attachments by hash. These +attachments are ZIP/JAR files containing arbitrary content. The information in these files can then be +used when checking the transaction's validity. + +Time-windows +^^^^^^^^^^^^ +In some cases, we want a transaction proposed to only be approved during a certain time-window. For example: + +* An option can only be exercised after a certain date +* A bond may only be redeemed before its expiry date + +In such cases, we can add a *time-window* to the transaction. Time-windows specify the time window during which the +transaction can be committed. We discuss time-windows in the section on :doc:`key-concepts-time-windows`. \ No newline at end of file diff --git a/docs/source/key-concepts.rst b/docs/source/key-concepts.rst index d54a13b936..fdf2b0231c 100644 --- a/docs/source/key-concepts.rst +++ b/docs/source/key-concepts.rst @@ -1,7 +1,28 @@ -Overview -======== +.. _key-concepts-label: -This section describes the key concepts and features of the Corda platform. +Key concepts +============ + +This section describes the key concepts and features of the Corda platform. It is intended for readers who are new to +Corda, and want to understand its architecture. It does not contain any code, and is suitable for non-developers. + +This section should be read in order: + +.. toctree:: + :maxdepth: 1 + + key-concepts-ecosystem + key-concepts-ledger + key-concepts-states + key-concepts-contracts + key-concepts-transactions + key-concepts-flows + key-concepts-consensus + key-concepts-notaries + key-concepts-time-windows + key-concepts-oracles + key-concepts-node + key-concepts-tradeoffs The detailed thinking and rationale behind these concepts are presented in two white papers: diff --git a/docs/source/loadtesting.rst b/docs/source/loadtesting.rst index c28ff78142..35c0364deb 100644 --- a/docs/source/loadtesting.rst +++ b/docs/source/loadtesting.rst @@ -31,9 +31,13 @@ In order to run the loadtests you need to have an active SSH-agent running with You can use either IntelliJ or the gradle command line to start the tests. -To use gradle: ``./gradlew tools:loadtest:run -Ploadtest-config=PATH_TO_LOADTEST_CONF`` +To use gradle with configuration file: ``./gradlew tools:loadtest:run -Ploadtest-config=PATH_TO_LOADTEST_CONF`` -To use IntelliJ simply run Main.kt with the config path supplied as an argument. +To use gradle with system properties: ``./gradlew tools:loadtest:run -Dloadtest.mode=LOAD_TEST -Dloadtest.nodeHosts.0=node0.myhost.com`` + +.. note:: You can provide or override any configuration using the system properties, all properties will need to be prefixed with ``loadtest.``. + +To use IntelliJ simply run Main.kt with the config path supplied as an argument or system properties as vm options. Configuration of individual load tests -------------------------------------- @@ -112,3 +116,12 @@ The ``gatherRemoteState`` function should check the actual remote nodes' states The reason it gets the previous state boils down to allowing non-deterministic predictions about the nodes' remote states. Say some piece of work triggers an asynchronous notification of a node. We need to account both for the case when the node hasn't received the notification and for the case when it has. In these cases ``S`` should somehow represent a collection of possible states, and ``gatherRemoteState`` should "collapse" the collection based on the observations it makes. Of course we don't need this for the simple case of the Self Issue test. The last parameter ``isConsistent`` is used to poll for eventual consistency at the end of a load test. This is not needed for self-issuance. + +Stability Test +-------------- + +Stability test is one variation of the load test, instead of flooding the nodes with request, the stability test uses execution frequency limit to achieve a constant execution rate. + +To run the stability test, set the load test mode to STABILITY_TEST (``mode=STABILITY_TEST`` in config file or ``-Dloadtest.mode=STABILITY_TEST`` in system properties). + +The stability test will first self issue cash using ``StabilityTest.selfIssueTest`` and after that it will randomly pay and exit cash using ``StabilityTest.crossCashTest`` for P2P testing, unlike the load test, the stability test will run without any disruption. \ No newline at end of file diff --git a/docs/source/merkle-trees.rst b/docs/source/merkle-trees.rst deleted file mode 100644 index 6c8378fa1f..0000000000 --- a/docs/source/merkle-trees.rst +++ /dev/null @@ -1,125 +0,0 @@ -Transaction tear-offs -===================== - -One of the basic data structures in our platform is a transaction. It can be passed around to be signed and verified, -also by third parties. The construction of transactions assumes that they form a whole entity with input and output states, -commands and attachments inside. However all sensitive data shouldn’t be revealed to other nodes that take part in -the creation of transaction on validation level (a good example of this situation is the Oracle which validates only -embedded commands). How to achieve it in a way that convinces the other party the data they got for signing really did form -a part of the transaction? - -We decided to use well known and described cryptographic scheme to provide proofs of inclusion and data integrity. -Merkle trees are widely used in peer-to-peer networks, blockchain systems and git. -You can read more on the concept `here `_. - -Merkle trees in Corda ---------------------- - -Transactions are split into leaves, each of them contains either input, output, command or attachment. Additionally, in -transaction id calculation we use other fields of ``WireTransaction`` like timestamp, notary, type and signers. -Next, the Merkle tree is built in the normal way by hashing the concatenation of nodes’ hashes below the current one together. -It’s visible on the example image below, where ``H`` denotes sha256 function, "+" - concatenation. - -.. image:: resources/merkleTree.png - -The transaction has two input states, one of output, attachment and command each and timestamp. For brevity we didn't -include all leaves on the diagram (type, notary and signers are presented as one leaf labelled Rest - in reality -they are separate leaves). Notice that if a tree is not a full binary tree, leaves are padded to the nearest power -of 2 with zero hash (since finding a pre-image of sha256(x) == 0 is hard computational task) - marked light green above. -Finally, the hash of the root is the identifier of the transaction, it's also used for signing and verification of data integrity. -Every change in transaction on a leaf level will change its identifier. - -Hiding data ------------ - -Hiding data and providing the proof that it formed a part of a transaction is done by constructing Partial Merkle Trees -(or Merkle branches). A Merkle branch is a set of hashes, that given the leaves’ data, is used to calculate the root’s hash. -Then that hash is compared with the hash of a whole transaction and if they match it means that data we obtained belongs -to that particular transaction. - -.. image:: resources/partialMerkle.png - -In the example above, the node ``H(f)`` is the one holding command data for signing by Oracle service. Blue leaf ``H(g)`` is also -included since it's holding timestamp information. Nodes labelled ``Provided`` form the Partial Merkle Tree, black ones -are omitted. Having timestamp with the command that should be in a violet node place and branch we are able to calculate -root of this tree and compare it with original transaction identifier - we have a proof that this command and timestamp -belong to this transaction. - -Example of usage ----------------- - -Let’s focus on a code example. We want to construct a transaction with commands containing interest rate fix data as in: -:doc:`oracles`. -After construction of a partial transaction, with included ``Fix`` commands in it, we want to send it to the Oracle for checking -and signing. To do so we need to specify which parts of the transaction are going to be revealed. That can be done by constructing -filtering function over fields of ``WireTransaction`` of type ``(Any) -> Boolean``. - -.. container:: codeset - - .. sourcecode:: kotlin - - val partialTx = ... - val oracle: Party = ... - fun filtering(elem: Any): Boolean { - return when (elem) { - is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix - else -> false - } - } - -Assuming that we already assembled partialTx with some commands and know the identity of Oracle service, -we construct filtering function over commands - ``filtering``. It performs type checking and filters only ``Fix`` commands -as in IRSDemo example. Then we can construct ``FilteredTransaction``: - -.. container:: codeset - - .. sourcecode:: kotlin - - val wtx: WireTransaction = partialTx.toWireTransaction() - val ftx: FilteredTransaction = wtx.buildFilteredTransaction(filtering) - -In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: :ref:`filtering_ref` - - -``FilteredTransaction`` holds ``filteredLeaves`` (data that we wanted to reveal) and Merkle branch for them. - -.. container:: codeset - - .. sourcecode:: kotlin - - // Direct accsess to included commands, inputs, outputs, attachments etc. - val cmds: List = ftx.filteredLeaves.commands - val ins: List = ftx.filteredLeaves.inputs - val timestamp: Timestamp? = ftx.filteredLeaves.timestamp - ... - -.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt - :language: kotlin - :start-after: DOCSTART 1 - :end-before: DOCEND 1 - -Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. -You can check only leaves using ``leaves.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` -to see if data from ``PartialMerkleTree`` belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash -of the full transaction: - -.. container:: codeset - - .. sourcecode:: kotlin - - if (!ftx.verify(merkleRoot)){ - throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") - } - -Or combine the two steps together: - -.. container:: codeset - - .. sourcecode:: kotlin - - ftx.verifyWithFunction(merkleRoot, ::check) - -.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove - leaves. However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. - As signing is done now over the Merkle root hash, the service signs all commands of given type, even though it didn't see - all of them. This issue will be handled after implementing partial signatures. diff --git a/docs/source/network-simulator.rst b/docs/source/network-simulator.rst index 0385572713..0d0916d68e 100644 --- a/docs/source/network-simulator.rst +++ b/docs/source/network-simulator.rst @@ -40,7 +40,7 @@ to single JVM simulations. Interface --------- -.. image:: network-simulator.png +.. image:: resources/network-simulator.png The network simulator can be run automatically, or stepped manually through each step of the interest rate swap. The options on the simulator window are: diff --git a/docs/source/node-internals-index.rst b/docs/source/node-internals-index.rst new file mode 100644 index 0000000000..8fe4795988 --- /dev/null +++ b/docs/source/node-internals-index.rst @@ -0,0 +1,11 @@ +Node internals +============== + +.. toctree:: + :maxdepth: 1 + + node-services + vault + serialization + messaging + persistence \ No newline at end of file diff --git a/docs/source/node-services.rst b/docs/source/node-services.rst index 059f8e0070..b2db166b8f 100644 --- a/docs/source/node-services.rst +++ b/docs/source/node-services.rst @@ -1,5 +1,5 @@ -Brief introduction to the node services -======================================= +Node services +============= This document is intended as a very brief introduction to the current service components inside the node. Whilst not at all exhaustive it is diff --git a/docs/source/oracles.rst b/docs/source/oracles.rst index 63afdf510f..e77147893a 100644 --- a/docs/source/oracles.rst +++ b/docs/source/oracles.rst @@ -171,7 +171,7 @@ Let's see what parameters we pass to the constructor of this oracle. 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 the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService +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. @@ -195,41 +195,37 @@ Here we can see that there are several steps: exactly our data source. The final step, assuming we have got this far, is to generate a signature for the transaction and return it. -Binding to the network via a CorDapp plugin -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Binding to the network +~~~~~~~~~~~~~~~~~~~~~~ .. note:: Before reading any further, we advise that you understand the concept of flows and how to write them and use them. See :doc:`flow-state-machines`. Likewise some understanding of Cordapps, plugins and services will be helpful. - See :doc:`creating-a-cordapp`. + See :doc:`running-a-node`. -The first step is to create a service to host the oracle on the network. Let's see how that's implemented: +The first step is to create the oracle as a service by annotating its class with ``@CordaService``. Let's see how that's +done: + +.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt + :language: kotlin + :start-after: DOCSTART 3 + :end-before: DOCEND 3 + +The Corda node scans for any class with this annotation and initialises them. The only requirement is that the class provide +a constructor with a single parameter of type ``PluginServiceHub```. In our example the oracle class has two constructors. +The second is used for testing. .. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt :language: kotlin :start-after: DOCSTART 2 :end-before: DOCEND 2 -This may look complicated, but really it's made up of some relatively simple elements (in the order they appear in the code): +These two flows leverage the oracle to provide the querying and signing operations. They get reference to the oracle, +which will have already been initialised by the node, using ``ServiceHub.cordappService``. Both flows are annotated with +``@InitiatedBy``. This tells the node which initiating flow (which are discussed in the next section) they are meant to +be executed with. -1. Accept a ``PluginServiceHub`` in the constructor. This is your interface to the Corda node. -2. Ensure you extend the abstract class ``SingletonSerializeAsToken`` (see :doc:`corda-plugins`). -3. Create an instance of your core oracle class that has the ``query`` and ``sign`` methods as discussed above. -4. Register your client sub-flows (in this case both in ``RatesFixFlow``. See the next section) for querying and - signing as initiating your service flows that actually do the querying and signing using your core oracle class instance. -5. Implement your service flows that call your core oracle class instance. - -The final step is to register your service with the node via the plugin mechanism. Do this by -implementing a plugin. Don't forget the resources file to register it with the ``ServiceLoader`` framework -(see :doc:`corda-plugins`). - -.. sourcecode:: kotlin - - class Plugin : CordaPluginRegistry() { - override val servicePlugins: List> = listOf(Service::class.java) - } - -Providing client sub-flows for querying and signing -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Providing sub-flows for querying and signing +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ We mentioned the client sub-flow briefly above. They are the mechanism that clients, in the form of other flows, will interact with your oracle. Typically there will be one for querying and one for signing. Let's take a look at @@ -279,3 +275,11 @@ Here's an example of it in action from ``FixingFlow.Fixer``. When overriding be careful when making the sub-class an anonymous or inner class (object declarations in Kotlin), because that kind of classes can access variables from the enclosing scope and cause serialization problems when checkpointed. + +Testing +------- + +When unit testing, we make use of the ``MockNetwork`` which allows us to create ``MockNode`` instances. A ``MockNode`` +is a simplified node suitable for tests. One feature that isn't available (and which is not suitable in unit testing +anyway) is the node's ability to scan and automatically install oracles it finds in the CorDapp jars. Instead, when +working with ``MockNode``, use the ``installCordaService`` method to manually install the oracle on the relevant node. \ No newline at end of file diff --git a/docs/source/other-index.rst b/docs/source/other-index.rst new file mode 100644 index 0000000000..5fd4d78554 --- /dev/null +++ b/docs/source/other-index.rst @@ -0,0 +1,12 @@ +Other +===== + +.. toctree:: + :maxdepth: 1 + + clauses + merkle-trees + json + secure-coding-guidelines + corda-repo-layout + building-the-docs \ No newline at end of file diff --git a/docs/source/out-of-process-verification.rst b/docs/source/out-of-process-verification.rst index 90e3f6befc..269fc37eaf 100644 --- a/docs/source/out-of-process-verification.rst +++ b/docs/source/out-of-process-verification.rst @@ -1,4 +1,4 @@ -Out of process verification +Out-of-process verification =========================== A Corda node does transaction verification through ``ServiceHub.transactionVerifierService``. This is by default an diff --git a/docs/source/permissioning.rst b/docs/source/permissioning.rst index 1a75104a7f..420188c73d 100644 --- a/docs/source/permissioning.rst +++ b/docs/source/permissioning.rst @@ -22,8 +22,6 @@ The following information from the node configuration file is needed to generate .. note:: In a future version the uniqueness requirement will be relaxed to a X.500 name. This will allow differentiation between entities with the same name. -:nearestCity: e.g. "London" - :emailAddress: e.g. "admin@company.com" :certificateSigningService: Doorman server URL. A doorman server will be hosted by R3 in the near diff --git a/docs/source/quickstart-index.rst b/docs/source/quickstart-index.rst new file mode 100644 index 0000000000..12404a0d8f --- /dev/null +++ b/docs/source/quickstart-index.rst @@ -0,0 +1,10 @@ +Quickstart +========== + +.. toctree:: + :maxdepth: 1 + + getting-set-up + tutorial-cordapp + running-the-demos + CLI-vs-IDE \ No newline at end of file diff --git a/docs/source/release-notes.rst b/docs/source/release-notes.rst index 02a2ab14cf..0251c6907f 100644 --- a/docs/source/release-notes.rst +++ b/docs/source/release-notes.rst @@ -6,13 +6,52 @@ Here are release notes for each snapshot release from M9 onwards. Unreleased ---------- -We've added the ability for flows to be versioned by their CorDapp developers. This enables a node to support a particular -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. +Certificate checks have been enabled for much of the identity service, with additional checks coming targetted at M13. +These are part of the confidential identities work, and ensure that parties are actually who they claim to be by checking +their certificate path back to the network trust root (certificate authority). -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 12 - First Public Beta +-------------------------------- +One of our busiest releases, lots of changes that take us closer to API stability (for more detailed information about +what has changed, see :doc:`changelog`). In this release we focused mainly on making developers' lives easier. Taking +into account feedback from numerous training courses and meet-ups, we decided to add ``CollectSignaturesFlow`` which +factors out a lot of code which CorDapp developers needed to write to get their transactions signed. +The improvement is up to 150 fewer lines of code in each flow! To have your transaction signed by different parties, you +need only now call a subflow which collects the parties' signatures for you. + +Additionally we introduced classpath scanning to wire-up flows automatically. Writing CorDapps has been made simpler by +removing boiler-plate code that was previously required when registering flows. Writing services such as oracles has also been simplified. + +We made substantial RPC performance improvements (please note that this is separate to node performance, we are focusing +on that area in future milestones): + +- 15-30k requests per second for a single client/server RPC connection. + * 1Kb requests, 1Kb responses, server and client on same machine, parallelism 8, measured on a Dell XPS 17(i7-6700HQ, 16Gb RAM) +- The framework is now multithreaded on both client and server side. +- All remaining bottlenecks are in the messaging layer. + +Security of the key management service has been improved by removing support for extracting private keys, in order that +it can support use of a hardware security module (HSM) for key storage. Instead it exposes functionality for signing data +(typically transactions). The service now also supports multiple signature schemes (not just EdDSA). + +We've added the beginnings of flow versioning. Nodes now reject flow requests if the initiating side is not using the same +flow version. In a future milestone release will add the ability to support backwards compatibility. + +As with the previous few releases we have continued work extending identity support. 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. + +Added new Byzantine fault tolerant (BFT) decentralised notary demo, based on the `BFT-SMaRT protocol `_ +For how to run the demo see: :ref:`notary-demo` + +We continued to work on tools that enable diagnostics on the node. The newest addition to Corda Shell is ``flow watch`` command which +lets the administrator see all flows currently running with result or error information as well as who is the flow initiator. +Here is the view from DemoBench: + +.. image:: resources/flowWatchCmd.png + +We also started work on the strategic wire format (not integrated). Milestone 11 ------------ @@ -29,7 +68,7 @@ distinguished names (see RFC 1779 for details on the construction of distinguish enforced, however it will be in a later milestone. * "myLegalName" in node configurations will need to be replaced, for example "Bank A" is replaced with - "CN=Bank A,O=Bank A,L=London,C=UK". Obviously organisation, location and country ("O", "L" and "C" respectively) + "CN=Bank A,O=Bank A,L=London,C=GB". Obviously organisation, location and country ("O", "L" and "C" respectively) must be given values which are appropriate to the node, do not just use these example values. * "networkMap" in node configurations must be updated to match any change to the legal name of the network map. * If you are using mock parties for testing, try to standardise on the ``DUMMY_NOTARY``, ``DUMMY_BANK_A``, etc. provided diff --git a/docs/source/release-process-index.rst b/docs/source/release-process-index.rst new file mode 100644 index 0000000000..ff8d39ea52 --- /dev/null +++ b/docs/source/release-process-index.rst @@ -0,0 +1,10 @@ +Release process +=============== + +.. toctree:: + :maxdepth: 1 + + release-notes + changelog + publishing-corda + codestyle \ No newline at end of file diff --git a/docs/source/resources/basic-tx.png b/docs/source/resources/basic-tx.png new file mode 100644 index 0000000000..90b5271055 Binary files /dev/null and b/docs/source/resources/basic-tx.png differ diff --git a/docs/source/resources/cheatsheet.jpg b/docs/source/resources/cheatsheet.jpg index 2a8f513308..e63f1df17d 100644 Binary files a/docs/source/resources/cheatsheet.jpg and b/docs/source/resources/cheatsheet.jpg differ diff --git a/docs/source/resources/commands.png b/docs/source/resources/commands.png new file mode 100644 index 0000000000..d5b2052815 Binary files /dev/null and b/docs/source/resources/commands.png differ diff --git a/docs/source/resources/committed_tx.png b/docs/source/resources/committed_tx.png new file mode 100644 index 0000000000..eaee83f82c Binary files /dev/null and b/docs/source/resources/committed_tx.png differ diff --git a/docs/source/contract-cp-state.png b/docs/source/resources/contract-cp-state.png similarity index 100% rename from docs/source/contract-cp-state.png rename to docs/source/resources/contract-cp-state.png diff --git a/docs/source/contract-cp.png b/docs/source/resources/contract-cp.png similarity index 100% rename from docs/source/contract-cp.png rename to docs/source/resources/contract-cp.png diff --git a/docs/source/contract-irs.png b/docs/source/resources/contract-irs.png similarity index 100% rename from docs/source/contract-irs.png rename to docs/source/resources/contract-irs.png diff --git a/docs/source/resources/flow-overview.png b/docs/source/resources/flow-overview.png new file mode 100644 index 0000000000..c76019627a Binary files /dev/null and b/docs/source/resources/flow-overview.png differ diff --git a/docs/source/resources/flow-sequence.png b/docs/source/resources/flow-sequence.png new file mode 100644 index 0000000000..c2898a6c50 Binary files /dev/null and b/docs/source/resources/flow-sequence.png differ diff --git a/docs/source/resources/flow.gif b/docs/source/resources/flow.gif new file mode 100644 index 0000000000..8ec9f939b5 Binary files /dev/null and b/docs/source/resources/flow.gif differ diff --git a/docs/source/resources/flowFramework.png b/docs/source/resources/flowFramework.png deleted file mode 100644 index 639658506a..0000000000 Binary files a/docs/source/resources/flowFramework.png and /dev/null differ diff --git a/docs/source/resources/flowWatchCmd.png b/docs/source/resources/flowWatchCmd.png new file mode 100644 index 0000000000..4434d90fd8 Binary files /dev/null and b/docs/source/resources/flowWatchCmd.png differ diff --git a/docs/source/resources/full-tx.png b/docs/source/resources/full-tx.png new file mode 100644 index 0000000000..6391496135 Binary files /dev/null and b/docs/source/resources/full-tx.png differ diff --git a/docs/source/resources/grouped-tx.png b/docs/source/resources/grouped-tx.png new file mode 100644 index 0000000000..d15441513b Binary files /dev/null and b/docs/source/resources/grouped-tx.png differ diff --git a/docs/source/resources/in-out-groups.png b/docs/source/resources/in-out-groups.png new file mode 100644 index 0000000000..deb3653110 Binary files /dev/null and b/docs/source/resources/in-out-groups.png differ diff --git a/docs/source/resources/ledger-table.png b/docs/source/resources/ledger-table.png new file mode 100644 index 0000000000..300ba68a80 Binary files /dev/null and b/docs/source/resources/ledger-table.png differ diff --git a/docs/source/resources/ledger-venn.png b/docs/source/resources/ledger-venn.png new file mode 100644 index 0000000000..af95f82456 Binary files /dev/null and b/docs/source/resources/ledger-venn.png differ diff --git a/docs/source/network-simulator.png b/docs/source/resources/network-simulator.png similarity index 100% rename from docs/source/network-simulator.png rename to docs/source/resources/network-simulator.png diff --git a/docs/source/resources/network.png b/docs/source/resources/network.png new file mode 100644 index 0000000000..5f5bff9d98 Binary files /dev/null and b/docs/source/resources/network.png differ diff --git a/docs/source/resources/node-architecture.png b/docs/source/resources/node-architecture.png new file mode 100644 index 0000000000..cfc8b22cf7 Binary files /dev/null and b/docs/source/resources/node-architecture.png differ diff --git a/docs/source/resources/node-diagram.png b/docs/source/resources/node-diagram.png new file mode 100644 index 0000000000..40ac9cebf4 Binary files /dev/null and b/docs/source/resources/node-diagram.png differ diff --git a/docs/source/resources/running_node.png b/docs/source/resources/running_node.png new file mode 100644 index 0000000000..a4e289a5fe Binary files /dev/null and b/docs/source/resources/running_node.png differ diff --git a/docs/source/resources/state-hierarchy.png b/docs/source/resources/state-hierarchy.png new file mode 100644 index 0000000000..a1c950683a Binary files /dev/null and b/docs/source/resources/state-hierarchy.png differ diff --git a/docs/source/resources/state-sequence.png b/docs/source/resources/state-sequence.png new file mode 100644 index 0000000000..af1e5dac4d Binary files /dev/null and b/docs/source/resources/state-sequence.png differ diff --git a/docs/source/resources/state.png b/docs/source/resources/state.png new file mode 100644 index 0000000000..fadac02564 Binary files /dev/null and b/docs/source/resources/state.png differ diff --git a/docs/source/resources/time-window.gif b/docs/source/resources/time-window.gif new file mode 100644 index 0000000000..8b0204488a Binary files /dev/null and b/docs/source/resources/time-window.gif differ diff --git a/docs/source/resources/transaction-flow.png b/docs/source/resources/transaction-flow.png new file mode 100644 index 0000000000..1860f80b76 Binary files /dev/null and b/docs/source/resources/transaction-flow.png differ diff --git a/docs/source/resources/tutorial-flow.png b/docs/source/resources/tutorial-flow.png new file mode 100644 index 0000000000..0d4c72b541 Binary files /dev/null and b/docs/source/resources/tutorial-flow.png differ diff --git a/docs/source/resources/tutorial-state.png b/docs/source/resources/tutorial-state.png new file mode 100644 index 0000000000..b394852fbe Binary files /dev/null and b/docs/source/resources/tutorial-state.png differ diff --git a/docs/source/resources/tutorial-transaction.png b/docs/source/resources/tutorial-transaction.png new file mode 100644 index 0000000000..0cb7ce2e49 Binary files /dev/null and b/docs/source/resources/tutorial-transaction.png differ diff --git a/docs/source/resources/tx-chain.png b/docs/source/resources/tx-chain.png new file mode 100644 index 0000000000..bd2114ac1a Binary files /dev/null and b/docs/source/resources/tx-chain.png differ diff --git a/docs/source/resources/tx-validation.png b/docs/source/resources/tx-validation.png new file mode 100644 index 0000000000..41602ac803 Binary files /dev/null and b/docs/source/resources/tx-validation.png differ diff --git a/docs/source/resources/tx_with_sigs.png b/docs/source/resources/tx_with_sigs.png new file mode 100644 index 0000000000..3db08479db Binary files /dev/null and b/docs/source/resources/tx_with_sigs.png differ diff --git a/docs/source/resources/uncommitted_tx.png b/docs/source/resources/uncommitted_tx.png new file mode 100644 index 0000000000..e3974b0697 Binary files /dev/null and b/docs/source/resources/uncommitted_tx.png differ diff --git a/docs/source/resources/ungrouped-tx.png b/docs/source/resources/ungrouped-tx.png new file mode 100644 index 0000000000..951752084b Binary files /dev/null and b/docs/source/resources/ungrouped-tx.png differ diff --git a/docs/source/resources/uniqueness-consensus.png b/docs/source/resources/uniqueness-consensus.png new file mode 100644 index 0000000000..206fbd1176 Binary files /dev/null and b/docs/source/resources/uniqueness-consensus.png differ diff --git a/docs/source/resources/validation-consensus.png b/docs/source/resources/validation-consensus.png new file mode 100644 index 0000000000..9269458f65 Binary files /dev/null and b/docs/source/resources/validation-consensus.png differ diff --git a/docs/source/resources/vault-simple.png b/docs/source/resources/vault-simple.png new file mode 100644 index 0000000000..485d7cd9ba Binary files /dev/null and b/docs/source/resources/vault-simple.png differ diff --git a/docs/source/creating-a-cordapp.rst b/docs/source/running-a-node.rst similarity index 75% rename from docs/source/creating-a-cordapp.rst rename to docs/source/running-a-node.rst index c4c808aeb5..9ec08fdc73 100644 --- a/docs/source/creating-a-cordapp.rst +++ b/docs/source/running-a-node.rst @@ -1,39 +1,12 @@ -CorDapp basics +Running a node ============== -A CorDapp is an application that runs on the Corda platform using the platform APIs and plugin system. They are self -contained in separate JARs from the node server JAR that are created and distributed. +Deploying your node +------------------- -App plugins ------------ - -.. note:: Currently apps are only supported for JVM languages. - -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. 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 --------- - -Services are classes which are constructed after the node has started. It is provided a `PluginServiceHub`_ which -allows a richer API than the `ServiceHub`_ exposed to contracts. It enables adding flows, registering -message handlers and more. The service does not run in a separate thread, so the only entry point to the service is during -construction, where message handlers should be registered and threads started. - - -Starting nodes --------------- - -To use an app you must also have a node server. To create a node server run the ``gradle deployNodes`` task. - -This will output the node JAR to ``build/libs/corda.jar`` and several sample/standard -node setups to ``build/nodes``. For now you can use the ``build/nodes/nodea`` configuration as a template. +You deploy a node by running the ``gradle deployNodes`` task. This will output the node JAR to +``build/libs/corda.jar`` and several sample/standard node setups to ``build/nodes``. For now you can use the +``build/nodes/nodea`` configuration as a template. Each node server by default must have a ``node.conf`` file in the current working directory. After first execution of the node server there will be many other configuration and persistence files created in this @@ -45,14 +18,6 @@ workspace directory. The directory can be overridden by the ``--base-directory=< temporary folder. It is therefore suggested that the CAPSULE_CACHE_DIR environment variable be set before starting the process to control this location. -Installing apps ---------------- - -Once you have created your app JAR you can install it to a node by adding it to ``/plugins/``. In this -case the ``node_dir`` is the location where your node server's JAR and configuration file is. - -.. note:: If the directory does not exist you can create it manually. - Starting your node ------------------ @@ -186,17 +151,17 @@ deployment. To use this gradle plugin you must add a new task that is of the type ``net.corda.plugins.Cordform`` to your build.gradle and then configure the nodes you wish to deploy with the Node and nodes configuration DSL. -This DSL is specified in the `JavaDoc `_. An example of this is in the CorDapp template and below +This DSL is specified in the `JavaDoc `_. An example of this is in the CorDapp template and +below is a three node example; .. code-block:: text task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // The output directory - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" // The distinguished name of the node named here will be used as the networkMapService.address on all other nodes. + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" // The distinguished name of the node named here will be used as the networkMapService.address on all other nodes. node { - name "CN=Controller,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Controller,O=R3,OU=corda,L=London,C=GB" advertisedServices = [ "corda.notary.validating" ] p2pPort 10002 rpcPort 10003 @@ -205,8 +170,7 @@ is a three node example; cordapps [] } node { - name "CN=NodeA,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=NodeA,O=R3,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -216,7 +180,6 @@ is a three node example; } node { name "CN=NodeB,O=R3,OU=corda,L=New York,C=US" - nearestCity "New York" advertisedServices = [] p2pPort 10008 rpcPort 10009 diff --git a/docs/source/running-the-demos.rst b/docs/source/running-the-demos.rst index 206a68fc48..871037ead0 100644 --- a/docs/source/running-the-demos.rst +++ b/docs/source/running-the-demos.rst @@ -1,7 +1,8 @@ Running the demos ================= -The Corda repository contains a number of demo programs demonstrating the functionality developed so far: +The `Corda repository `_ contains a number of demo programs demonstrating +Corda's functionality: 1. The Trader Demo, which shows a delivery-vs-payment atomic swap of commercial paper for cash 2. The IRS Demo, which shows two nodes establishing an interest rate swap and performing fixings with a @@ -9,16 +10,16 @@ The Corda repository contains a number of demo programs demonstrating the functi 3. The Attachment Demo, which demonstrates uploading attachments to nodes 4. The SIMM Valuation Demo, which shows two nodes agreeing on a portfolio and valuing the initial margin using the Standard Initial Margin Model -5. The Distributed Notary Demo, which shows a single node getting multiple transactions notarised by a distributed (Raft-based) notary +5. The Notary Demo, which shows three different types of notaries and a single node getting multiple transactions notarised. 6. The Bank of Corda Demo, which shows a node acting as an issuer of assets (the Bank of Corda) while remote client applications request issuance of some cash on behalf of a node called Big Corporation -We recommend running the demos from the command line rather than from IntelliJ. For more details about running via the command line or from within IntelliJ, see :doc:`CLI-vs-IDE`. - If any of the demos don't work, please raise an issue on GitHub. .. note:: If you are running the demos from the command line in Linux (but not macOS), you may have to install xterm. +.. note:: If you would like to see flow activity on the nodes type in the node terminal ``flow watch``. + .. _trader-demo: Trader demo @@ -32,18 +33,18 @@ To run from the command line in Unix: 1. Run ``./gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples/trader-demo/build/nodes`` 2. Run ``./samples/trader-demo/build/nodes/runnodes`` to open up four new terminals with the four nodes 3. Run ``./gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node. - This step will display progress information related to the cash issuance process (in the bank of corda node log output) -4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. You can see both sides of the - trade print their progress and final transaction state in the bank node tabs/windows +4. Run ``./gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` +you can see flows running on both sides of transaction. Additionally you should see final trade information displayed +to your terminal. To run from the command line in Windows: 1. Run ``gradlew samples:trader-demo:deployNodes`` to create a set of configs and installs under ``samples\trader-demo\build\nodes`` 2. Run ``samples\trader-demo\build\nodes\runnodes`` to open up four new terminals with the four nodes 3. Run ``gradlew samples:trader-demo:runBuyer`` to instruct the buyer node to request issuance of some cash from the Bank of Corda node. - This step will display progress information related to the cash issuance process (in the Bank of Corda node log output) -4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. You can see both sides of the - trade print their progress and final transaction state in the bank node tabs/windows +4. Run ``gradlew samples:trader-demo:runSeller`` to trigger the transaction. If you entered ``flow watch`` +you can see flows running on both sides of transaction. Additionally you should see final trade information displayed +to your terminal. .. _irs-demo: @@ -63,8 +64,8 @@ To run from the command line in Unix: 5. Run ``./install/irs-demo/bin/irs-demo --role UploadRates``. You should see a message be printed to the first node (the notary/oracle/network map node) saying that it has accepted the new interest rates -6. Now run ``./install/irs-demo/bin/irs-demo --role Trade 1``. The number is a trade ID. You should - see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on +6. Now run ``./install/irs-demo/bin/irs-demo --role Trade 1``. The number is a trade ID. If you enter in node's terminal + ``flow watch`` you should see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on 7. Now run ``./install/irs-demo/bin/irs-demo --role Date 2017-12-12`` to roll the simulated clock forward and see some fixings take place To run from the command line in Windows: @@ -76,8 +77,8 @@ To run from the command line in Windows: 5. Run ``install\irs-demo\bin\irs-demo --role UploadRates``. You should see a message be printed to the first node (the notary/oracle/network map node) saying that it has accepted the new interest rates -6. Now run ``install\irs-demo\bin\irs-demo --role Trade 1``. The number is a trade ID. You should - see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on +6. Now run ``install\irs-demo\bin\irs-demo --role Trade 1``. The number is a trade ID. If you enter in node's terminal + ``flow watch`` you should see lots of activity as the nodes set up the deal, notarise it, get it signed by the oracle, and so on 7. Now run ``install\irs-demo\bin\irs-demo --role Date 2017-12-12`` to roll the simulated clock forward and see some fixings take place This demo also has a web app. To use this, run nodes and upload rates, then navigate to @@ -86,6 +87,9 @@ http://localhost:10007/web/irsdemo and http://localhost:10010/web/irsdemo to see To use the web app, click the "Create Deal" button, fill in the form, then click the "Submit" button. You can then use the time controls at the top left of the home page to run the fixings. Click any individual trade in the blotter to view it. +.. note:: The IRS web UI currently has a bug when changing the clock time where it may show no numbers or apply fixings inconsistently. + The issues will be addressed in M13 milestone release. Meanwhile, you can take a look at a simpler oracle example https://github.com/corda/oracle-example + Attachment demo --------------- @@ -94,7 +98,7 @@ This demo brings up three nodes, and sends a transaction containing an attachmen To run from the command line in Unix: 1. Run ``./gradlew samples:attachment-demo:deployNodes`` to create a set of configs and installs under ``samples/attachment-demo/build/nodes`` -2. Run ``./samples/attachment-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three nodes +2. Run ``./samples/attachment-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three nodes and webserver for BankB 3. Run ``./gradlew samples:attachment-demo:runRecipient``, which will block waiting for a trade to start 4. Run ``./gradlew samples:attachment-demo:runSender`` in another terminal window to send the attachment. Now look at the other windows to see the output of the demo @@ -102,11 +106,13 @@ To run from the command line in Unix: To run from the command line in Windows: 1. Run ``gradlew samples:attachment-demo:deployNodes`` to create a set of configs and installs under ``samples\attachment-demo\build\nodes`` -2. Run ``samples\attachment-demo\build\nodes\runnodes`` to open up three new terminal tabs/windows with the three nodes +2. Run ``samples\attachment-demo\build\nodes\runnodes`` to open up three new terminal tabs/windows with the three nodes and webserver for BankB 3. Run ``gradlew samples:attachment-demo:runRecipient``, which will block waiting for a trade to start 4. Run ``gradlew samples:attachment-demo:runSender`` in another terminal window to send the attachment. Now look at the other windows to see the output of the demo +.. _notary-demo: + Notary demo ----------- @@ -123,21 +129,26 @@ You will notice that successive transactions get signed by different members of To run the Raft version of the demo from the command line in Unix: -1. Run ``./gradlew samples:notary-demo:deployNodesRaft``, which will create node directories with configs under ``samples/notary-demo/build/nodes``. -2. Run ``./samples/notary-demo/build/nodes/runnodes``, which will start the nodes in separate terminal windows/tabs. +1. Run ``./gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and + Single notaries). +2. Run ``./samples/notary-demo/build/nodes/nodesRaft/runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``./gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys To run from the command line in Windows: -1. Run ``gradlew samples:notary-demo:deployNodesRaft``, which will create node directories with configs under ``samples\notary-demo\build\nodes``. -2. Run ``samples\notary-demo\build\nodes\runnodes``, which will start the nodes in separate terminal windows/tabs. +1. Run ``gradlew samples:notary-demo:deployNodes``, which will create all three types of notaries' node directories + with configs under ``samples/notary-demo/build/nodes/nodesRaft`` (``nodesBFT`` and ``nodesSingle`` for BFT and + Single notaries). +2. Run ``samples\notary-demo\build\nodes\nodesRaft\runnodes``, which will start the nodes in separate terminal windows/tabs. Wait until a "Node started up and registered in ..." message appears on each of the terminals 3. Run ``gradlew samples:notary-demo:notarise`` to make a call to the "Party" node to initiate notarisation requests In a few seconds you will see a message "Notarised 10 transactions" with a list of transaction ids and the signer public keys -To run the BFT SMaRt notary demo, use ``deployNodesBFT`` instead of ``deployNodesRaft``. For a single notary node, use ``deployNodesSingle``. +To run the BFT SMaRt notary demo, use ``nodesBFT`` instead of ``nodesRaft`` in the path (you will see messages from notary nodes +trying to communicate each other sometime with connection errors, that's normal). For a single notary node, use ``nodesSingle``. Notary nodes store consumed states in a replicated commit log, which is backed by a H2 database on each node. You can ascertain that the commit log is synchronised across the cluster by accessing and comparing each of the nodes' backing stores @@ -180,7 +191,7 @@ To run from the command line in Unix: 2. Run ``./samples/bank-of-corda-demo/build/nodes/runnodes`` to open up three new terminal tabs/windows with the three nodes 3. Run ``./gradlew samples:bank-of-corda-demo:runRPCCashIssue`` to trigger a cash issuance request 4. Run ``./gradlew samples:bank-of-corda-demo:runWebCashIssue`` to trigger another cash issuance request. - Now look at the Bank of Corda terminal tab/window to see the output of the demo + Now look at your terminal tab/window to see the output of the demo To run from the command line in Windows: @@ -188,27 +199,24 @@ To run from the command line in Windows: 2. Run ``samples\bank-of-corda-demo\build\nodes\runnodes`` to open up three new terminal tabs/windows with the three nodes 3. Run ``gradlew samples:bank-of-corda-demo:runRPCCashIssue`` to trigger a cash issuance request 4. Run ``gradlew samples:bank-of-corda-demo:runWebCashIssue`` to trigger another cash issuance request. - Now look at the Bank of Corda terminal tab/window to see the output of the demo + Now look at the your terminal tab/window to see the output of the demo .. note:: To verify that the Bank of Corda node is alive and running, navigate to the following URL: http://localhost:10007/api/bank/date .. note:: The Bank of Corda node explicitly advertises with a node service type as follows: - ``advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer"))))`` + ``advertisedServices = ["corda.issuer.USD"]`` This allows for 3rd party applications to perform actions based on Node Type. For example, the Explorer tool only allows nodes of this type to issue and exit cash. -In the "Bank Of Corda Demo: Run Issuer" window, you should see the following progress steps displayed: - -- Awaiting issuance request -- Self issuing asset -- Transferring asset to issuance requester -- Confirming asset issuance to requester - -In the client issue request window, you should see the following printed: +In the window you run the command you should see (in case of Web, RPC is simmilar): +- Requesting Cash via Web ... - Successfully processed Cash Issue request +If you want to see flow activity enter in node's shell ``flow watch``. It will display all state machines +running currently on the node. + Launch the Explorer application to visualize the issuance and transfer of cash for each node: ``./gradlew tools:explorer:run`` (on Unix) or ``gradlew tools:explorer:run`` (on Windows) diff --git a/docs/source/secure-coding-guidelines.rst b/docs/source/secure-coding-guidelines.rst index 98d24a283d..285f17412f 100644 --- a/docs/source/secure-coding-guidelines.rst +++ b/docs/source/secure-coding-guidelines.rst @@ -33,7 +33,8 @@ allowed to do anything! Things to watch out for: sides of the flow. The theme should be clear: signing is a very sensitive operation, so you need to be sure you know what it is you -are about to sign, and that nothing has changed in the small print! +are about to sign, and that nothing has changed in the small print! Once you have provided your signature over a +transaction to a counterparty, there is no longer anything you can do to prevent them from committing it to the ledger. Contracts --------- diff --git a/docs/source/serialization.rst b/docs/source/serialization.rst index 8e973559f4..0bc5340d61 100644 --- a/docs/source/serialization.rst +++ b/docs/source/serialization.rst @@ -1,4 +1,4 @@ -Object Serialization +Object serialization ==================== What is serialization (and deserialization)? @@ -23,7 +23,7 @@ Classes get onto the whitelist via one of three mechanisms: class itself, on any of the super classes or on any interface implemented by the class or super classes or any interface extended by an interface implemented by the class or superclasses. #. By returning the class as part of a plugin via the method ``customizeSerialization``. It's important to return - true from this method if you override it, otherwise the plugin will be excluded. See :doc:`corda-plugins`. + true from this method if you override it, otherwise the plugin will be excluded. See :doc:`writing-cordapps`. #. Via the built in Corda whitelist (see the class ``DefaultWhitelist``). Whilst this is not user editable, it does list common JDK classes that have been whitelisted for your convenience. diff --git a/docs/source/setting-up-a-corda-network.rst b/docs/source/setting-up-a-corda-network.rst index d7b57744a2..18a2172058 100644 --- a/docs/source/setting-up-a-corda-network.rst +++ b/docs/source/setting-up-a-corda-network.rst @@ -1,6 +1,6 @@ .. _log4j2: http://logging.apache.org/log4j/2.x/ -What is a Corda network? +Creating a Corda network ======================== A Corda network consists of a number of machines running nodes, including a single node operating as the network map diff --git a/docs/source/shell.rst b/docs/source/shell.rst index 3e53ae1113..f9489591a2 100644 --- a/docs/source/shell.rst +++ b/docs/source/shell.rst @@ -39,8 +39,9 @@ Starting flows and performing remote method calls ------------------------------------------------- **Flows** are the way the ledger is changed. If you aren't familiar with them, please review ":doc:`flow-state-machines`" -first. The ``flow list`` command can be used to list the flows understood by the node and ``flow start`` can be -used to start them. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and +first. The ``flow list`` command can be used to list the flows understood by the node, ``flow watch`` shows all the flows +currently running on the node with the result (or error) information in a user friendly way, ``flow start`` can be +used to start flows. The ``flow start`` command takes the class name of a flow, or *any unambiguous substring* and then the data to be passed to the flow constructor. The unambiguous substring feature is helpful for reducing the needed typing. If the match is ambiguous the possible matches will be printed out. If a flow has multiple constructors then the names and types of the arguments will be used to try and determine which to use automatically. @@ -67,7 +68,7 @@ Yaml (yet another markup language) is a simple JSON-like way to describe object that make it helpful for our use case, like a lightweight syntax and support for "bare words" which mean you can often skip the quotes around strings. Here is an example of how this syntax is used: -``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "CN=Bank A,O=Bank A,L=London,C=UK", notary: "CN=Notary Service,O=R3,OU=corda,L=London,C=UK"`` +``flow start CashIssue amount: $1000, issueRef: 1234, recipient: "CN=Bank A,O=Bank A,L=London,C=GB", notary: "CN=Notary Service,O=R3,OU=corda,L=London,C=GB"`` This invokes a constructor of a flow with the following prototype in the code: diff --git a/docs/source/tools-index.rst b/docs/source/tools-index.rst new file mode 100644 index 0000000000..36d24534a0 --- /dev/null +++ b/docs/source/tools-index.rst @@ -0,0 +1,11 @@ +Tools +===== + +.. toctree:: + :maxdepth: 1 + + network-simulator + demobench + node-explorer + azure-vm + loadtesting \ No newline at end of file diff --git a/docs/source/getting-set-up-fault-finding.rst b/docs/source/troubleshooting.rst similarity index 100% rename from docs/source/getting-set-up-fault-finding.rst rename to docs/source/troubleshooting.rst diff --git a/docs/source/tutorial-clientrpc-api.rst b/docs/source/tutorial-clientrpc-api.rst index 32fb39c465..517bd1ca66 100644 --- a/docs/source/tutorial-clientrpc-api.rst +++ b/docs/source/tutorial-clientrpc-api.rst @@ -1,7 +1,7 @@ .. _graphstream: http://graphstream-project.org/ -Client RPC API tutorial -======================= +Using the client RPC API +======================== 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 @@ -104,7 +104,7 @@ requests or responses with the Corda node. Here's an example of both ways you c :start-after: START 7 :end-before: END 7 -See more on plugins in :doc:`creating-a-cordapp`. +See more on plugins in :doc:`running-a-node`. .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. @@ -125,7 +125,7 @@ In the instructions above the server node permissions are configured programmati driver(driverDirectory = baseDirectory) { val user = User("user", "password", permissions = setOf(startFlowPermission())) - val node = startNode("CN=Alice Corp,O=Alice Corp,L=London,C=UK", rpcUsers = listOf(user)).get() + val node = startNode("CN=Alice Corp,O=Alice Corp,L=London,C=GB", rpcUsers = listOf(user)).get() When starting a standalone node using a configuration file we must supply the RPC credentials as follows: @@ -162,4 +162,4 @@ With regards to the start flow RPCs, there is an extra layer of security whereby 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` +Cordformation in :doc:`running-a-node`. diff --git a/docs/source/tutorial-contract-clauses.rst b/docs/source/tutorial-contract-clauses.rst index 7957dd4d9f..cb6ea53f46 100644 --- a/docs/source/tutorial-contract-clauses.rst +++ b/docs/source/tutorial-contract-clauses.rst @@ -136,7 +136,7 @@ and is included in the ``CommercialPaper.kt`` code. 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. @@ -167,7 +167,7 @@ and is included in the ``CommercialPaper.kt`` code. // 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. diff --git a/docs/source/tutorial-contract.rst b/docs/source/tutorial-contract.rst index 8fe5836d4a..b41f65aa25 100644 --- a/docs/source/tutorial-contract.rst +++ b/docs/source/tutorial-contract.rst @@ -30,7 +30,7 @@ value of the commercial paper. This lifecycle for commercial paper is illustrated in the diagram below: -.. image:: contract-cp.png +.. image:: resources/contract-cp.png Where to put your code ---------------------- @@ -61,7 +61,7 @@ Kotlin syntax works. class CommercialPaper : Contract { override val legalContractReference: SecureHash = SecureHash.sha256("https://en.wikipedia.org/wiki/Commercial_paper"); - override fun verify(tx: TransactionForVerification) { + override fun verify(tx: TransactionForContract) { TODO() } } @@ -75,7 +75,7 @@ Kotlin syntax works. } @Override - public void verify(TransactionForVerification tx) { + public void verify(TransactionForContract tx) { throw new UnsupportedOperationException(); } } @@ -97,7 +97,7 @@ States A state is a class that stores data that is checked by the contract. A commercial paper state is structured as below: -.. image:: contract-cp-state.png +.. image:: resources/contract-cp-state.png .. container:: codeset @@ -106,14 +106,14 @@ A state is a class that stores data that is checked by the contract. A commercia data class State( val issuance: PartyAndReference, - override val owner: PublicKey, + override val owner: AbstractParty, val faceValue: Amount>, val maturityDate: Instant ) : OwnableState { override val contract = CommercialPaper() override val participants = listOf(owner) - fun withoutOwner() = copy(owner = NullPublicKey) + fun withoutOwner() = copy(owner = AnonymousParty(NullPublicKey)) override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) } @@ -121,7 +121,7 @@ A state is a class that stores data that is checked by the contract. A commercia public static class State implements OwnableState { private PartyAndReference issuance; - private PublicKey owner; + private AbstractParty owner; private Amount> faceValue; private Instant maturityDate; @@ -142,7 +142,7 @@ A state is a class that stores data that is checked by the contract. A commercia @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)); } @@ -150,7 +150,7 @@ A state is a class that stores data that is checked by the contract. A commercia return issuance; } - public PublicKey getOwner() { + public AbstractParty getOwner() { return owner; } @@ -192,7 +192,7 @@ A state is a class that stores data that is checked by the contract. A commercia @NotNull @Override - public List getParticipants() { + public List getParticipants() { return ImmutableList.of(this.owner); } } @@ -214,7 +214,7 @@ We have four fields in our state: relationships such as a derivative contract. * ``faceValue``, an ``Amount>``, which wraps an integer number of pennies and a currency that is specific to some issuer (e.g. a regular bank, a central bank, etc). You can read more about this very common - type in :doc:`key-concepts-core-types`. + type in :doc:`api-core-types`. * ``maturityDate``, an `Instant `_, which is a type from the Java 8 standard time library. It defines a point on the timeline. @@ -408,7 +408,7 @@ blank out fields that are allowed to change, making the grouping key be "everyth .. sourcecode:: kotlin - val groups = tx.groupStates() { it: State -> it.withoutOwner() } + val groups = tx.groupStates(State::withoutOwner) .. sourcecode:: java @@ -431,15 +431,15 @@ logic. .. sourcecode:: kotlin - val timestamp: Timestamp? = tx.timestamp + val timeWindow: TimeWindow? = tx.timeWindow for ((inputs, outputs, key) in groups) { when (command.value) { 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 state is propagated" using (group.outputs.size == 1) + "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. } @@ -449,18 +449,18 @@ logic. // Redemption of the paper requires movement of on-ledger cash. val input = inputs.single() val received = tx.outputs.sumCashBy(input.owner) - val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped") + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must be timestamped") requireThat { "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) } } is Commands.Issue -> { val output = outputs.single() - val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must be timestamped") requireThat { // Don't allow people to issue commercial paper under other entities identities. "output states are issued by a command signer" using (output.issuance.party.owningKey in command.signers) @@ -492,12 +492,14 @@ logic. // 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. } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Redeem) { - checkNotNull(timem "must be timestamped"); - Instant t = time.getBefore(); + TimeWindow timeWindow = tx.getTimeWindow(); + Instant time = null == timeWindow + ? null + : timeWindow.getUntilTime(); Amount> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); checkState(received.equals(input.getFaceValue()), "received amount equals the face value"); - checkState(t.isBefore(input.getMaturityDate(), "the paper must have matured"); + checkState(time != null && !time.isBefore(input.getMaturityDate(), "the paper must have matured"); checkState(outputs.isEmpty(), "the paper must be destroyed"); } else if (cmd.getValue() instanceof JavaCommercialPaper.Commands.Issue) { // .. etc .. (see Kotlin for full definition) @@ -613,7 +615,7 @@ a method to wrap up the issuance process: 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)) } @@ -641,7 +643,7 @@ any ``StateAndRef`` (input), ``ContractState`` (output) or ``Command`` objects a for you. There's one final thing to be aware of: we ask the caller to select a *notary* that controls this state and -prevents it from being double spent. You can learn more about this topic in the :doc:`key-concepts-consensus-notaries` article. +prevents it from being double spent. You can learn more about this topic in the :doc:`key-concepts-notaries` article. .. note:: For now, don't worry about how to pick a notary. More infrastructure will come later to automate this decision for you. @@ -652,10 +654,10 @@ What about moving the paper, i.e. reassigning ownership to someone else? .. sourcecode:: kotlin - 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)) } Here, the method takes a pre-existing ``TransactionBuilder`` and adds to it. This is correct because typically @@ -678,28 +680,28 @@ Finally, we can do redemption. .. sourcecode:: kotlin @Throws(InsufficientBalanceException::class) - fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, wallet: Wallet) { - // Add the cash movement using the states in our wallet. - Cash().generateSpend(tx, paper.state.data.faceValue.withoutIssuer(), paper.state.data.owner, wallet.statesOfType()) + fun generateRedeem(tx: TransactionBuilder, paper: StateAndRef, vault: VaultService) { + // 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)) } Here we can see an example of composing contracts together. When an owner wishes to redeem the commercial paper, the -issuer (i.e. the caller) must gather cash from its wallet and send the face value to the owner of the paper. +issuer (i.e. the caller) must gather cash from its vault and send the face value to the owner of the paper. .. note:: This contract has no explicit concept of rollover. -The *wallet* is a concept that may be familiar from Bitcoin and Ethereum. It is simply a set of states (such as cash) that are -owned by the caller. Here, we use the wallet to update the partial transaction we are handed with a movement of cash -from the issuer of the commercial paper to the current owner. If we don't have enough quantity of cash in our wallet, +The *vault* is a concept that may be familiar from Bitcoin and Ethereum. It is simply a set of states (such as cash) that are +owned by the caller. Here, we use the vault to update the partial transaction we are handed with a movement of cash +from the issuer of the commercial paper to the current owner. If we don't have enough quantity of cash in our vault, an exception is thrown. Then we add the paper itself as an input, but, not an output (as we wish to remove it from the ledger). Finally, we add a Redeem command that should be signed by the owner of the commercial paper. .. warning:: The amount we pass to the ``generateSpend`` function has to be treated first with ``withoutIssuer``. This reflects the fact that the way we handle issuer constraints is still evolving; the commercial paper contract requires payment in the form of a currency issued by a specific party (e.g. the central bank, - or the issuers own bank perhaps). But the wallet wants to assemble spend transactions using cash states from + or the issuers own bank perhaps). But the vault wants to assemble spend transactions using cash states from any issuer, thus we must strip it here. This represents a design mismatch that we will resolve in future versions with a more complete way to express issuer constraints. @@ -745,7 +747,7 @@ Making things happen at a particular time It would be nice if you could program your node to automatically redeem your commercial paper as soon as it matures. Corda provides a way for states to advertise scheduled events that should occur in future. Whilst this information is by default ignored, if the corresponding *Cordapp* is installed and active in your node, and if the state is -considered relevant by your wallet (e.g. because you own it), then the node can automatically begin the process +considered relevant by your vault (e.g. because you own it), then the node can automatically begin the process of creating a transaction and taking it through the life cycle. You can learn more about this in the article ":doc:`event-scheduling`". diff --git a/docs/source/tutorial-cordapp.rst b/docs/source/tutorial-cordapp.rst index 1dd25730e2..97ee76af97 100644 --- a/docs/source/tutorial-cordapp.rst +++ b/docs/source/tutorial-cordapp.rst @@ -448,7 +448,7 @@ Navigate to the "create IOU" button at the top left of the page, and enter the I Order Number: 1 Delivery Date: 2018-09-15 City: London - Country Code: UK + Country Code: GB Item name: Wow such item Item amount: 5 @@ -623,7 +623,8 @@ The example CorDapp has the following directory structure: │ │   └── resources │ │   ├── META-INF │ │   │   └── services - │ │   │   └── net.corda.core.node.CordaPluginRegistry + │   │   │   ├── net.corda.core.node.CordaPluginRegistry + │   │ │ └── net.corda.webserver.services.WebServerPluginRegistry │ │   ├── certificates │ │   │   ├── readme.txt │ │   │   ├── sslkeystore.jks @@ -665,7 +666,8 @@ The example CorDapp has the following directory structure: │   └── resources │   ├── META-INF │   │   └── services - │   │   └── net.corda.core.node.CordaPluginRegistry +    │   │   ├── net.corda.core.node.CordaPluginRegistry +    │ │ └── net.corda.webserver.services.WebServerPluginRegistry │   ├── certificates │   │   ├── readme.txt │   │   ├── sslkeystore.jks @@ -774,10 +776,9 @@ like to deploy for testing. See further details below: task deployNodes(type: com.r3corda.plugins.Cordform, dependsOn: ['jar']) { directory "./kotlin-source/build/nodes" // The output directory. - networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK" // The distinguished name of the node to be used as the network map. + networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB" // The distinguished name of the node to be used as the network map. node { - name "CN=Controller,O=R3,OU=corda,L=London,C=UK" // Distinguished name of node to be deployed. - nearestCity "London" // For use with the network visualiser. + name "CN=Controller,O=R3,OU=corda,L=London,C=GB" // Distinguished name of node to be deployed. advertisedServices = ["corda.notary.validating"] // A list of services you wish the node to offer. p2pPort 10002 rpcPort 10003 // Usually 1 higher than the messaging port. @@ -785,8 +786,7 @@ like to deploy for testing. See further details below: cordapps = [] // Add package names of CordaApps. } node { // Create an additional node. - name "CN=NodeA,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=NodeA,O=R3,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -810,7 +810,8 @@ Service Provider Configuration File ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If you are building a CorDapp from scratch or adding a new CorDapp to the cordapp-tutorial project then you must provide -a reference to your sub-class of ``CordaPluginRegistry`` in the provider-configuration file in located in the ``resources/META-INF/services`` directory. +a reference to your sub-class of ``CordaPluginRegistry`` or ``WebServerPluginRegistry`` (for Wep API) in the provider-configuration file +located in the ``resources/META-INF/services`` directory. Re-Deploying Your Nodes Locally ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/source/tutorial-tear-offs.rst b/docs/source/tutorial-tear-offs.rst new file mode 100644 index 0000000000..4e247f733d --- /dev/null +++ b/docs/source/tutorial-tear-offs.rst @@ -0,0 +1,80 @@ +Transaction tear-offs +===================== + +Example of usage +---------------- +Let’s focus on a code example. We want to construct a transaction with commands containing interest rate fix data as in: +:doc:`oracles`. After construction of a partial transaction, with included ``Fix`` commands in it, we want to send it +to the Oracle for checking and signing. To do so we need to specify which parts of the transaction are going to be +revealed. That can be done by constructing filtering function over fields of ``WireTransaction`` of type ``(Any) -> +Boolean``. + +.. container:: codeset + + .. sourcecode:: kotlin + + val partialTx = ... + val oracle: Party = ... + fun filtering(elem: Any): Boolean { + return when (elem) { + is Command -> oracleParty.owningKey in elem.signers && elem.value is Fix + else -> false + } + } + +Assuming that we already assembled partialTx with some commands and know the identity of Oracle service, we construct +filtering function over commands - ``filtering``. It performs type checking and filters only ``Fix`` commands as in +IRSDemo example. Then we can construct ``FilteredTransaction``: + +.. container:: codeset + + .. sourcecode:: kotlin + + val wtx: WireTransaction = partialTx.toWireTransaction() + val ftx: FilteredTransaction = wtx.buildFilteredTransaction(filtering) + +In the Oracle example this step takes place in ``RatesFixFlow`` by overriding ``filtering`` function, see: +:ref:`filtering_ref`. + +``FilteredTransaction`` holds ``filteredLeaves`` (data that we wanted to reveal) and Merkle branch for them. + +.. container:: codeset + + .. sourcecode:: kotlin + + // Direct accsess to included commands, inputs, outputs, attachments etc. + val cmds: List = ftx.filteredLeaves.commands + val ins: List = ftx.filteredLeaves.inputs + val timestamp: Timestamp? = ftx.filteredLeaves.timestamp + ... + +.. literalinclude:: ../../samples/irs-demo/src/main/kotlin/net/corda/irs/api/NodeInterestRates.kt + :language: kotlin + :start-after: DOCSTART 1 + :end-before: DOCEND 1 + +Above code snippet is taken from ``NodeInterestRates.kt`` file and implements a signing part of an Oracle. You can +check only leaves using ``leaves.checkWithFun { check(it) }`` and then verify obtained ``FilteredTransaction`` to see +if data from ``PartialMerkleTree`` belongs to ``WireTransaction`` with provided ``id``. All you need is the root hash +of the full transaction: + +.. container:: codeset + + .. sourcecode:: kotlin + + if (!ftx.verify(merkleRoot)){ + throw MerkleTreeException("Rate Fix Oracle: Couldn't verify partial Merkle tree.") + } + +Or combine the two steps together: + +.. container:: codeset + + .. sourcecode:: kotlin + + ftx.verifyWithFunction(merkleRoot, ::check) + +.. note:: The way the ``FilteredTransaction`` is constructed ensures that after signing of the root hash it's impossible to add or remove + leaves. However, it can happen that having transaction with multiple commands one party reveals only subset of them to the Oracle. + As signing is done now over the Merkle root hash, the service signs all commands of given type, even though it didn't see + all of them. This issue will be handled after implementing partial signatures. diff --git a/docs/source/tutorial-test-dsl.rst b/docs/source/tutorial-test-dsl.rst index 6e6d98c9ab..51aa2789c8 100644 --- a/docs/source/tutorial-test-dsl.rst +++ b/docs/source/tutorial-test-dsl.rst @@ -56,7 +56,7 @@ We will start with defining helper function that returns a ``CommercialPaper`` s 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 ) @@ -68,7 +68,7 @@ We will start with defining helper function that returns a ``CommercialPaper`` s private ICommercialPaperState getPaper() { return new JavaCommercialPaper.State( getMEGA_CORP().ref(defaultRef), - getMEGA_CORP_PUBKEY(), + getMEGA_CORP(), issuedBy(DOLLARS(1000), getMEGA_CORP().ref(defaultRef)), getTEST_TX_TIME().plus(7, ChronoUnit.DAYS) ); diff --git a/docs/source/tutorials-index.rst b/docs/source/tutorials-index.rst new file mode 100644 index 0000000000..bd92064680 --- /dev/null +++ b/docs/source/tutorials-index.rst @@ -0,0 +1,22 @@ +Tutorials +========= + +.. toctree:: + :maxdepth: 1 + + hello-world-index + tutorial-contract + tutorial-contract-clauses + tutorial-test-dsl + contract-upgrade + tutorial-integration-testing + tutorial-clientrpc-api + tutorial-building-transactions + flow-state-machines + flow-testing + running-a-notary + using-a-notary + oracles + tutorial-tear-offs + tutorial-attachments + event-scheduling \ No newline at end of file diff --git a/docs/source/key-concepts-vault.rst b/docs/source/vault.rst similarity index 100% rename from docs/source/key-concepts-vault.rst rename to docs/source/vault.rst diff --git a/docs/source/writing-cordapps.rst b/docs/source/writing-cordapps.rst new file mode 100644 index 0000000000..b3a360efb5 --- /dev/null +++ b/docs/source/writing-cordapps.rst @@ -0,0 +1,145 @@ +Writing a CorDapp +================= + +The source-code for a CorDapp is a set of files written in a JVM language that defines a set of Corda components: + +* States (i.e. classes implementing ``ContractState``) +* Contracts (i.e. classes implementing ``Contract``) +* Flows (i.e. classes extending ``FlowLogic``) +* Web APIs +* Services + +These files should be placed under ``src/main/[java|kotlin]``. The CorDapp's resources folder (``src/main/resources``) +should also include the following subfolders: + +* ``src/main/resources/certificates``, containing the node's certificates +* ``src/main/resources/META-INF/services``, containing a file named ``net.corda.core.node.CordaPluginRegistry`` + +For example, the source-code of the `Template CorDapp `_ has the following +structure: + +.. parsed-literal:: + + src + ├── main + │ ├── java + │ │ └── com + │ │ └── template + │ │ ├── Main.java + │ │ ├── api + │ │ │ └── TemplateApi.java + │ │ ├── client + │ │ │ └── TemplateClientRPC.java + │ │ ├── contract + │ │ │ └── TemplateContract.java + │ │ ├── flow + │ │ │ └── TemplateFlow.java + │ │ ├── plugin + │ │ │ └── TemplatePlugin.java + │ │ ├── service + │ │ │ └── TemplateService.java + │ │ └── state + │ │ └── TemplateState.java + │ └── resources + │ ├── META-INF + │ │ └── services + │ │ ├── net.corda.core.node.CordaPluginRegistry + │ │ └── net.corda.webserver.services.WebServerPluginRegistry + │ ├── certificates + │ │ ├── sslkeystore.jks + │ │ └── truststore.jks + │ └──templateWeb + │ ├── index.html + │ └── js + │ └── template-js.js + └── test + └── java + └── com + └── template + └── contract + └── TemplateTests.java + +Defining a plugin +----------------- +You can specify the transport options (between nodes and between Web Client and a node) for your CorDapp by subclassing +``net.corda.core.node.CordaPluginRegistry``: + +* The ``customizeSerialization`` function allows classes to be whitelisted for object serialisation, over and + above those tagged with the ``@CordaSerializable`` annotation. For instance, new state types will need to be + explicitly registered. In general, the annotation should be preferred. See :doc:`serialization`. + +The fully-qualified class path of each ``CordaPluginRegistry`` subclass must be added to the +``net.corda.core.node.CordaPluginRegistry`` file in the CorDapp's ``resources/META-INF/services`` folder. A CorDapp +can register multiple plugins in a single ``net.corda.core.node.CordaPluginRegistry`` file. + +You can specify the web APIs and static web content for your CorDapp by implementing +``net.corda.webserver.services.WebServerPluginRegistry`` interface: + +* The ``webApis`` property is a list of JAX-RS annotated REST access classes. These classes will be constructed by + the bundled web server and must have a single argument constructor taking a ``CordaRPCOps`` object. This will + allow the API to communicate with the node process via the RPC interface. These web APIs will not be available if the + bundled web server is not started. + +* The ``staticServeDirs`` property maps static web content to virtual paths and allows simple web demos to be + distributed within the CorDapp jars. These static serving directories will not be available if the bundled web server + is not started. + * The static web content itself should be placed inside the ``src/main/resources`` directory + +The fully-qualified class path of each ``WebServerPluginRegistry`` class must be added to the +``net.corda.webserver.services.WebServerPluginRegistry`` file in the CorDapp's ``resources/META-INF/services`` folder. A CorDapp +can register multiple plugins in a single ``net.corda.webserver.services.WebServerPluginRegistry`` file. + +Installing CorDapps +------------------- +To run a CorDapp, its source is compiled into a JAR by running the gradle ``jar`` task. The CorDapp JAR is then added +to a node by adding it to the node's ``/plugins/`` folder (where ``node_dir`` is the folder in which the +node's JAR and configuration files are stored). + +.. note:: Any external dependencies of your CorDapp will automatically be placed into the + ``/dependencies/`` folder. This will be changed in a future release. + +.. note:: Building nodes using the gradle ``deployNodes`` task will place the CorDapp JAR into each node's ``plugins`` + folder automatically. + +At runtime, nodes will load any plugins present in their ``plugins`` folder. + +RPC permissions +--------------- +If a node's owner needs to interact with their node via RPC (e.g. to read the contents of the node's storage), they +must define one or more RPC users. These users are added to the node's ``node.conf`` file. + +The syntax for adding an RPC user is: + +.. container:: codeset + + .. sourcecode:: groovy + + rpcUsers=[ + { + username=exampleUser + password=examplePass + permissions=[] + } + ... + ] + +Currently, users need special permissions to start flows via RPC. These permissions are added as follows: + +.. container:: codeset + + .. sourcecode:: groovy + + rpcUsers=[ + { + username=exampleUser + password=examplePass + permissions=[ + "StartFlow.net.corda.flows.ExampleFlow1", + "StartFlow.net.corda.flows.ExampleFlow2" + ] + } + ... + ] + +.. note:: Currently, the node's web server has super-user access, meaning that it can run any RPC operation without + logging in. This will be changed in a future release. diff --git a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java index 3c427527c6..91d455b0d2 100644 --- a/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java +++ b/experimental/sandbox/src/test/java/net/corda/sandbox/WhitelistClassLoaderTest.java @@ -67,6 +67,8 @@ public class WhitelistClassLoaderTest { assertNotNull("Created object appears to be null", o); } + //TODO This code frequently throws StackOverflowException, despite this being explicitly what the code is trying to prevent!! + @Ignore @Test(expected = ClassNotFoundException.class) public void given_OverlyDeeplyTransitivelyLinkedClasses_then_ClassCanBeLoaded() throws Exception { Class clz = wlcl.loadClass("transitive.Chain4498"); diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt index 0d168828ce..73d7737eaa 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Arrangement.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.contracts.Frequency +import net.corda.contracts.Frequency import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt index 1e05ae571c..9d75a4ed30 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Literal.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal -import net.corda.core.contracts.BusinessCalendar -import net.corda.core.contracts.Frequency +import net.corda.contracts.BusinessCalendar +import net.corda.contracts.Frequency import net.corda.core.identity.Party import java.math.BigDecimal import java.time.LocalDate diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt index e28831228d..f5d25d5031 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Perceivable.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal -import net.corda.core.contracts.BusinessCalendar -import net.corda.core.contracts.Tenor +import net.corda.contracts.BusinessCalendar +import net.corda.contracts.Tenor import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import java.lang.reflect.Type 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 f86753bfba..74e3b0b00c 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/UniversalContract.kt @@ -1,5 +1,7 @@ package net.corda.contracts.universal +import net.corda.contracts.BusinessCalendar +import net.corda.contracts.FixOf import net.corda.core.contracts.* import net.corda.core.crypto.SecureHash import net.corda.core.identity.AbstractParty @@ -18,7 +20,7 @@ class UniversalContract : Contract { interface Commands : CommandData { - data class Fix(val fixes: List) : Commands + data class Fix(val fixes: List) : Commands // transition according to business rules defined in contract data class Action(val name: String) : Commands @@ -47,8 +49,8 @@ class UniversalContract : Contract { is PerceivableOr -> eval(tx, expr.left) || eval(tx, expr.right) is Const -> expr.value is TimePerceivable -> when (expr.cmp) { - Comparison.LTE -> tx.timestamp!!.after!! <= eval(tx, expr.instant) - Comparison.GTE -> tx.timestamp!!.before!! >= eval(tx, expr.instant) + Comparison.LTE -> tx.timeWindow!!.fromTime!! <= eval(tx, expr.instant) + Comparison.GTE -> tx.timeWindow!!.untilTime!! >= eval(tx, expr.instant) else -> throw NotImplementedError("eval special") } is ActorPerceivable -> tx.commands.single().signers.contains(expr.actor.owningKey) @@ -207,7 +209,7 @@ class UniversalContract : Contract { assert(rest is Zero) requireThat { - "action must be timestamped" using (tx.timestamp != null) + "action must have a time-window" using (tx.timeWindow != null) // "action must be authorized" by (cmd.signers.any { action.actors.any { party -> party.owningKey == it } }) // todo perhaps merge these two requirements? "condition must be met" using (eval(tx, action.condition)) diff --git a/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt b/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt index a187c07d10..c07257802b 100644 --- a/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt +++ b/experimental/src/main/kotlin/net/corda/contracts/universal/Util.kt @@ -2,7 +2,7 @@ package net.corda.contracts.universal import com.google.common.collect.ImmutableSet import com.google.common.collect.Sets -import net.corda.core.contracts.Frequency +import net.corda.contracts.Frequency import net.corda.core.identity.Party import java.security.PublicKey import java.time.Instant 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 5d275c2876..e73e8d96db 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Cap.kt @@ -1,9 +1,9 @@ package net.corda.contracts.universal -import net.corda.core.contracts.BusinessCalendar -import net.corda.core.contracts.FixOf -import net.corda.core.contracts.Frequency -import net.corda.core.contracts.Tenor +import net.corda.contracts.BusinessCalendar +import net.corda.contracts.FixOf +import net.corda.contracts.Frequency +import net.corda.contracts.Tenor import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -167,7 +167,7 @@ class Cap { fun issue() { transaction { output { stateInitial } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -187,7 +187,7 @@ class Cap { transaction { input { stateInitial } output { stateAfterFixingFirst } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -196,32 +196,32 @@ class Cap { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this.verifies() } @@ -234,7 +234,7 @@ class Cap { output { stateAfterExecutionFirst } output { statePaymentFirst } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -253,7 +253,7 @@ class Cap { input { stateAfterFixingFinal } output { statePaymentFinal } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -271,7 +271,7 @@ class Cap { transaction { input { stateAfterExecutionFirst } output { stateAfterFixingFinal } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -280,32 +280,32 @@ class Cap { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01").plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", BusinessCalendar.parseDateFromString("2017-03-01"), Tenor("3M")), 1.0.bd))) } this.verifies() } 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 0c69005b22..63e955647e 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Caplet.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal -import net.corda.core.contracts.FixOf -import net.corda.core.contracts.Tenor +import net.corda.contracts.FixOf +import net.corda.contracts.Tenor import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -54,7 +54,7 @@ class Caplet { fun issue() { transaction { output { stateStart } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -74,7 +74,7 @@ class Caplet { transaction { input { stateFixed } output { stateFinal } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -92,7 +92,7 @@ class Caplet { transaction { input { stateStart } output { stateFixed } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -101,32 +101,32 @@ class Caplet { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("6M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("6M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("6M")), 1.0.bd))) } this.verifies() } 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 9bea6c3c37..73abc393cb 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXFwdTimeOption.kt @@ -51,7 +51,7 @@ class FXFwdTimeOption fun `issue - signature`() { transaction { output { inState } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -77,7 +77,7 @@ class FXFwdTimeOption output { outState1 } output { outState2 } - timestamp(TEST_TX_TIME_AFTER_MATURITY) + timeWindow(TEST_TX_TIME_AFTER_MATURITY) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -109,7 +109,7 @@ class FXFwdTimeOption output { outState1 } output { outState2 } - timestamp(TEST_TX_TIME_BEFORE_MATURITY) + timeWindow(TEST_TX_TIME_BEFORE_MATURITY) tweak { command(acmeCorp.owningKey) { UniversalContract.Commands.Action("some undefined name") } 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 272ae30171..755c9c2b98 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/FXSwap.kt @@ -43,7 +43,7 @@ class FXSwap { transaction { output { inState } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -68,7 +68,7 @@ class FXSwap { input { inState } output { outState1 } output { outState2 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -87,7 +87,7 @@ class FXSwap { input { inState } output { outState2 } output { outState1 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -106,7 +106,7 @@ class FXSwap { input { inState } output { outState1 } output { outState2 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "condition must be met" @@ -119,7 +119,7 @@ class FXSwap { input { inState } output { outState1 } output { outState2 } - timestamp(TEST_TX_TIME_TOO_EARLY) + timeWindow(TEST_TX_TIME_TOO_EARLY) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "condition must be met" @@ -131,7 +131,7 @@ class FXSwap { transaction { input { inState } output { outState1 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "output state must match action result state" @@ -144,7 +144,7 @@ class FXSwap { input { inState } output { outState1 } output { outStateBad2 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "output states must match action result state" @@ -157,7 +157,7 @@ class FXSwap { input { inState } output { outStateBad1 } output { outState2 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "output states must match action result state" @@ -170,7 +170,7 @@ class FXSwap { input { inState } output { outState1 } output { outStateBad3 } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "output states must match action result state" 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 e80cffc391..612dd47c25 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/IRS.kt @@ -1,8 +1,8 @@ package net.corda.contracts.universal -import net.corda.core.contracts.FixOf -import net.corda.core.contracts.Frequency -import net.corda.core.contracts.Tenor +import net.corda.contracts.FixOf +import net.corda.contracts.Frequency +import net.corda.contracts.Tenor import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -134,7 +134,7 @@ class IRS { fun issue() { transaction { output { stateInitial } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -154,7 +154,7 @@ class IRS { transaction { input { stateInitial } output { stateAfterFixingFirst } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -163,32 +163,32 @@ class IRS { tweak { // wrong source - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBORx", tradeDate, Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong date - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate.plusYears(1), Tenor("3M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { // wrong tenor - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("9M")), 1.0.bd))) } this `fails with` "relevant fixing must be included" } tweak { - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.5.bd))) } this `fails with` "output state does not reflect fix command" } - command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.core.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } + command(highStreetBank.owningKey) { UniversalContract.Commands.Fix(listOf(net.corda.contracts.Fix(FixOf("LIBOR", tradeDate, Tenor("3M")), 1.0.bd))) } this.verifies() } @@ -201,7 +201,7 @@ class IRS { output { stateAfterExecutionFirst } output { statePaymentFirst } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } 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 c91a78be11..a51fda60f4 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/RollOutTests.kt @@ -1,6 +1,6 @@ package net.corda.contracts.universal -import net.corda.core.contracts.Frequency +import net.corda.contracts.Frequency import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Test @@ -143,7 +143,7 @@ class RollOutTests { fun issue() { transaction { output { stateStart } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" @@ -164,7 +164,7 @@ class RollOutTests { input { stateStart } output { stateStep1a } output { stateStep1b } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) /* tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } 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 7dea92bc4f..5e89376f77 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/Swaption.kt @@ -1,7 +1,7 @@ package net.corda.contracts.universal -import net.corda.core.contracts.Frequency -import net.corda.core.contracts.Tenor +import net.corda.contracts.Frequency +import net.corda.contracts.Tenor import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.transaction import org.junit.Ignore @@ -60,7 +60,7 @@ class Swaption { fun issue() { transaction { output { stateInitial } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) this `fails with` "transaction has a single command" 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 458bacb4bb..09b4aef290 100644 --- a/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt +++ b/experimental/src/test/kotlin/net/corda/contracts/universal/ZeroCouponBond.kt @@ -70,7 +70,7 @@ class ZeroCouponBond { transaction { input { inState } output { outState } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) tweak { command(highStreetBank.owningKey) { UniversalContract.Commands.Action("some undefined name") } @@ -88,7 +88,7 @@ class ZeroCouponBond { transaction { input { inState } output { outState } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(momAndPop.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "condition must be met" @@ -100,7 +100,7 @@ class ZeroCouponBond { transaction { input { inState } output { outStateWrong } - timestamp(TEST_TX_TIME_1) + timeWindow(TEST_TX_TIME_1) command(acmeCorp.owningKey) { UniversalContract.Commands.Action("execute") } this `fails with` "output state must match action result state" diff --git a/finance/build.gradle b/finance/build.gradle index e7cf52b57d..6b9870ed39 100644 --- a/finance/build.gradle +++ b/finance/build.gradle @@ -29,3 +29,11 @@ configurations.testCompile { // TODO: Remove this exclusion once junit-quickcheck 0.8 is released. exclude group: 'javassist', module: 'javassist' } + +jar { + baseName 'corda-finance' +} + +publish { + name = jar.baseName +} \ No newline at end of file diff --git a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java index d2bb7eb3f8..bc1d505ff9 100644 --- a/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java +++ b/finance/src/main/java/net/corda/contracts/JavaCommercialPaper.java @@ -10,7 +10,6 @@ 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.*; @@ -20,7 +19,6 @@ import org.jetbrains.annotations.*; import java.time.*; import java.util.*; import java.util.stream.*; -import java.security.PublicKey; import static kotlin.collections.CollectionsKt.*; import static net.corda.core.contracts.ContractsDSL.*; @@ -206,14 +204,14 @@ public class JavaCommercialPaper implements Contract { 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(); - Instant time = null == timestamp + TimeWindow timeWindow = tx.getTimeWindow(); + Instant time = null == timeWindow ? null - : timestamp.getBefore(); + : timeWindow.getUntilTime(); Amount> received = CashKt.sumCashBy(tx.getOutputs(), input.getOwner()); requireThat(require -> { - require.using("must be timestamped", timestamp != null); + require.using("must be timestamped", timeWindow != null); require.using("received amount equals the face value: " + received + " vs " + input.getFaceValue(), received.equals(input.getFaceValue())); require.using("the paper must have matured", time != null && !time.isBefore(input.getMaturityDate())); @@ -243,15 +241,15 @@ public class JavaCommercialPaper implements Contract { State groupingKey) { AuthenticatedObject cmd = requireSingleCommand(tx.getCommands(), Commands.Issue.class); State output = single(outputs); - Timestamp timestampCommand = tx.getTimestamp(); - Instant time = null == timestampCommand + TimeWindow timeWindowCommand = tx.getTimeWindow(); + Instant time = null == timeWindowCommand ? null - : timestampCommand.getBefore(); + : timeWindowCommand.getUntilTime(); requireThat(require -> { require.using("output values sum to more than the inputs", inputs.isEmpty()); require.using("output values sum to more than the inputs", output.faceValue.getQuantity() > 0); - require.using("must be timestamped", timestampCommand != null); + require.using("must be timestamped", timeWindowCommand != null); require.using("the maturity date is not in the past", time != null && time.isBefore(output.getMaturityDate())); require.using("output states are issued by a command signer", cmd.getSigners().contains(output.issuance.getParty().getOwningKey())); return Unit.INSTANCE; diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt index 38c637ea1a..5f95d95aec 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaper.kt @@ -126,8 +126,8 @@ class CommercialPaper : Contract { groupingKey: Issued?): Set { val consumedCommands = super.verify(tx, inputs, outputs, commands, groupingKey) commands.requireSingleCommand() - val timestamp = tx.timestamp - val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") + val timeWindow = tx.timeWindow + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances must have a time-window") require(outputs.all { time < it.maturityDate }) { "maturity date is not in the past" } @@ -166,11 +166,11 @@ class CommercialPaper : Contract { // TODO: This should filter commands down to those with compatible subjects (underlying product and maturity date) // before requiring a single command val command = commands.requireSingleCommand() - val timestamp = tx.timestamp + val timeWindow = tx.timeWindow val input = inputs.single() val received = tx.outputs.sumCashBy(input.owner) - val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped") + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window") requireThat { "the paper must have matured" using (time >= input.maturityDate) "the received amount equals the face value" using (received == input.faceValue) diff --git a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt index 7be5b921f4..c8beea5c0e 100644 --- a/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt +++ b/finance/src/main/kotlin/net/corda/contracts/CommercialPaperLegacy.kt @@ -61,7 +61,7 @@ class CommercialPaperLegacy : Contract { // There are two possible things that can be done with this CP. The first is trading it. The second is redeeming // it for cash on or after the maturity date. val command = tx.commands.requireSingleCommand() - val timestamp: Timestamp? = tx.timestamp + val timeWindow: TimeWindow? = tx.timeWindow // Suppress compiler warning as 'key' is an unused variable when destructuring 'groups'. @Suppress("UNUSED_VARIABLE") @@ -81,7 +81,7 @@ class CommercialPaperLegacy : Contract { // Redemption of the paper requires movement of on-ledger cash. val input = inputs.single() val received = tx.outputs.sumCashBy(input.owner) - val time = timestamp?.after ?: throw IllegalArgumentException("Redemptions must be timestamped") + val time = timeWindow?.fromTime ?: throw IllegalArgumentException("Redemptions must have a time-window") requireThat { "the paper must have matured" using (time >= input.maturityDate) "the received amount equals the face value" using (received == input.faceValue) @@ -92,7 +92,7 @@ class CommercialPaperLegacy : Contract { is Commands.Issue -> { val output = outputs.single() - val time = timestamp?.before ?: throw IllegalArgumentException("Issuances must be timestamped") + val time = timeWindow?.untilTime ?: throw IllegalArgumentException("Issuances have a time-window") requireThat { // Don't allow people to issue commercial paper under other entities identities. "output states are issued by a command signer" using diff --git a/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt b/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt new file mode 100644 index 0000000000..2968cbf7d1 --- /dev/null +++ b/finance/src/main/kotlin/net/corda/contracts/FinanceTypes.kt @@ -0,0 +1,471 @@ +package net.corda.contracts + +import com.fasterxml.jackson.core.JsonGenerator +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.JsonDeserializer +import com.fasterxml.jackson.databind.JsonSerializer +import com.fasterxml.jackson.databind.SerializerProvider +import com.fasterxml.jackson.databind.annotation.JsonDeserialize +import com.fasterxml.jackson.databind.annotation.JsonSerialize +import net.corda.core.contracts.CommandData +import net.corda.core.contracts.LinearState +import net.corda.core.contracts.StateAndRef +import net.corda.core.contracts.TokenizableAssetInfo +import net.corda.core.identity.AbstractParty +import net.corda.core.identity.Party +import net.corda.core.node.services.ServiceType +import net.corda.core.node.services.VaultService +import net.corda.core.node.services.linearHeadsOfType +import net.corda.core.serialization.CordaSerializable +import net.corda.core.transactions.TransactionBuilder +import java.math.BigDecimal +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.DateTimeFormatter +import java.util.* + + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Interest rate fixes +// +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */ +@CordaSerializable +data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor) + +/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */ +data class Fix(val of: FixOf, val value: BigDecimal) : CommandData + +/** Represents a textual expression of e.g. a formula */ +@CordaSerializable +@JsonDeserialize(using = ExpressionDeserializer::class) +@JsonSerialize(using = ExpressionSerializer::class) +data class Expression(val expr: String) + +object ExpressionSerializer : JsonSerializer() { + override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) { + generator.writeString(expr.expr) + } +} + +object ExpressionDeserializer : JsonDeserializer() { + override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression { + return Expression(parser.text) + } +} + +/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */ +@CordaSerializable +data class Tenor(val name: String) { + private val amount: Int + private val unit: TimeUnit + + init { + if (name == "ON") { + // Overnight + amount = 1 + unit = TimeUnit.Day + } else { + val regex = """(\d+)([DMYW])""".toRegex() + val match = regex.matchEntire(name)?.groupValues ?: throw IllegalArgumentException("Unrecognised tenor name: $name") + + amount = match[1].toInt() + unit = TimeUnit.values().first { it.code == match[2] } + } + } + + fun daysToMaturity(startDate: LocalDate, calendar: BusinessCalendar): Int { + val maturityDate = when (unit) { + TimeUnit.Day -> startDate.plusDays(amount.toLong()) + TimeUnit.Week -> startDate.plusWeeks(amount.toLong()) + TimeUnit.Month -> startDate.plusMonths(amount.toLong()) + TimeUnit.Year -> startDate.plusYears(amount.toLong()) + else -> throw IllegalStateException("Invalid tenor time unit: $unit") + } + // Move date to the closest business day when it falls on a weekend/holiday + val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing) + val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual) + + return daysToMaturity + } + + override fun toString(): String = name + + @CordaSerializable + enum class TimeUnit(val code: String) { + Day("D"), Week("W"), Month("M"), Year("Y") + } +} + +/** + * Simple enum for returning accurals adjusted or unadjusted. + * We don't actually do anything with this yet though, so it's ignored for now. + */ +@CordaSerializable +enum class AccrualAdjustment { + Adjusted, Unadjusted +} + +/** + * This is utilised in the [DateRollConvention] class to determine which way we should initially step when + * finding a business day. + */ +@CordaSerializable +enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) } + +/** + * This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day. + * Depending on the accounting requirement, we can move forward until we get to a business day, or backwards. + * There are some additional rules which are explained in the individual cases below. + */ +@CordaSerializable +enum class DateRollConvention(val direction: () -> DateRollDirection, val isModified: Boolean) { + // direction() cannot be a val due to the throw in the Actual instance + + /** Don't roll the date, use the one supplied. */ + Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false), + /** Following is the next business date from this one. */ + Following({ DateRollDirection.FORWARD }, false), + /** + * "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding + * business date. + */ + ModifiedFollowing({ DateRollDirection.FORWARD }, true), + /** Previous is the previous business date from this one. */ + Previous({ DateRollDirection.BACKWARD }, false), + /** + * Modified previous is the previous business date, unless it's in the previous month, in which case use the next + * business date. + */ + ModifiedPrevious({ DateRollDirection.BACKWARD }, true); +} + + +/** + * This forms the day part of the "Day Count Basis" used for interest calculation. + * Note that the first character cannot be a number (enum naming constraints), so we drop that + * in the toString lest some people get confused. + */ +@CordaSerializable +enum class DayCountBasisDay { + // We have to prefix 30 etc with a letter due to enum naming constraints. + D30, + D30N, D30P, D30E, D30G, DActual, DActualJ, D30Z, D30F, DBus_SaoPaulo; + + override fun toString(): String { + return super.toString().drop(1) + } +} + +/** This forms the year part of the "Day Count Basis" used for interest calculation. */ +@CordaSerializable +enum class DayCountBasisYear { + // Ditto above comment for years. + Y360, + Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YICMA, Y252; + + override fun toString(): String { + return super.toString().drop(1) + } +} + +/** Whether the payment should be made before the due date, or after it. */ +@CordaSerializable +enum class PaymentRule { + InAdvance, InArrears, +} + +/** + * Frequency at which an event occurs - the enumerator also casts to an integer specifying the number of times per year + * that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc). + */ +@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed. +@CordaSerializable +enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) { + Annual(1, { plusYears(1 * it) }), + SemiAnnual(2, { plusMonths(6 * it) }), + Quarterly(4, { plusMonths(3 * it) }), + Monthly(12, { plusMonths(1 * it) }), + Weekly(52, { plusWeeks(1 * it) }), + BiWeekly(26, { plusWeeks(2 * it) }), + Daily(365, { plusDays(1 * it) }); +} + + +@Suppress("unused") // This utility may be useful in future. TODO: Review before API stability guarantees in place. +fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = accordingToCalendar.isWorkingDay(this) + +// TODO: Make Calendar data come from an oracle + +/** + * A business calendar performs date calculations that take into account national holidays and weekends. This is a + * typical feature of financial contracts, in which a business may not want a payment event to fall on a day when + * no staff are around to handle problems. + */ +@CordaSerializable +open class BusinessCalendar (val holidayDates: List) { + @CordaSerializable + class UnknownCalendar(name: String) : Exception("$name not found") + + companion object { + val calendars = listOf("London", "NewYork") + + val TEST_CALENDAR_DATA = calendars.map { + it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText() + }.toMap() + + /** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */ + fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE) + + /** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */ + fun getInstance(vararg calname: String) = BusinessCalendar( + calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }. + toSet(). + map { parseDateFromString(it) }. + toList().sorted() + ) + + /** Calculates an event schedule that moves events around to ensure they fall on working days. */ + fun createGenericSchedule(startDate: LocalDate, + period: Frequency, + calendar: BusinessCalendar = getInstance(), + dateRollConvention: DateRollConvention = DateRollConvention.Following, + noOfAdditionalPeriods: Int = Integer.MAX_VALUE, + endDate: LocalDate? = null, + periodOffset: Int? = null): List { + val ret = ArrayList() + var ctr = 0 + var currentDate = startDate + + while (true) { + currentDate = getOffsetDate(currentDate, period) + if (periodOffset == null || periodOffset <= ctr) + ret.add(calendar.applyRollConvention(currentDate, dateRollConvention)) + ctr += 1 + // TODO: Fix addl period logic + if ((ctr > noOfAdditionalPeriods) || (currentDate >= endDate ?: currentDate)) + break + } + return ret + } + + /** Calculates the date from @startDate moving forward @steps of time size @period. Does not apply calendar + * logic / roll conventions. + */ + fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate { + if (steps == 0) return startDate + return period.offset(startDate, steps.toLong()) + } + } + + override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) { + /** Note this comparison is OK as we ensure they are sorted in getInstance() */ + this.holidayDates == other.holidayDates + } else { + false + } + + override fun hashCode(): Int { + return this.holidayDates.hashCode() + } + + open fun isWorkingDay(date: LocalDate): Boolean = + when { + date.dayOfWeek == DayOfWeek.SATURDAY -> false + date.dayOfWeek == DayOfWeek.SUNDAY -> false + holidayDates.contains(date) -> false + else -> true + } + + open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate { + if (dateRollConvention == DateRollConvention.Actual) return testDate + + var direction = dateRollConvention.direction().value + var trialDate = testDate + while (!isWorkingDay(trialDate)) { + trialDate = trialDate.plusDays(direction) + } + + // We've moved to the next working day in the right direction, but if we're using the "modified" date roll + // convention and we've crossed into another month, reverse the direction instead to stay within the month. + // Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp + + if (dateRollConvention.isModified && testDate.month != trialDate.month) { + direction = -direction + trialDate = testDate + while (!isWorkingDay(trialDate)) { + trialDate = trialDate.plusDays(direction) + } + } + return trialDate + } + + /** + * Returns a date which is the inbound date plus/minus a given number of business days. + * TODO: Make more efficient if necessary + */ + fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate { + require(i >= 0) + if (i == 0) return date + var retDate = date + var ctr = 0 + while (ctr < i) { + retDate = retDate.plusDays(direction.value) + if (isWorkingDay(retDate)) ctr++ + } + return retDate + } +} + +fun calculateDaysBetween(startDate: LocalDate, + endDate: LocalDate, + dcbYear: DayCountBasisYear, + dcbDay: DayCountBasisDay): Int { + // Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later. + // TODO: The rest. + return when { + dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt() + dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt() + else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear") + } +} + +/** A common netting command for contracts whose states can be netted. */ +interface NetCommand : CommandData { + /** The type of netting to apply, see [NetType] for options. */ + val type: NetType +} + +/** + * Enum for the types of netting that can be applied to state objects. Exact behaviour + * for each type of netting is left to the contract to determine. + */ +@CordaSerializable +enum class NetType { + /** + * Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific), + * and allows their counterparty to net obligations without requiring approval from all parties. For example, if + * Bank A owes Bank B £1m, and Bank B owes Bank A £1m, in the case of Bank B defaulting this would enable Bank A + * to net out the two obligations to zero, rather than being legally obliged to pay £1m without any realistic + * expectation of the debt to them being paid. Realistically this is limited to bilateral netting, to simplify + * determining which party must sign the netting transaction. + */ + CLOSE_OUT, + /** + * "Payment" is used to refer to conventional netting, where all parties must confirm the netting transaction. This + * can be a multilateral netting transaction, and may be created by a central clearing service. + */ + PAYMENT +} + +/** + * Class representing a commodity, as an equivalent to the [Currency] class. This exists purely to enable the + * [CommodityContract] contract, and is likely to change in future. + * + * @param commodityCode a unique code for the commodity. No specific registry for these is currently defined, although + * this is likely to change in future. + * @param displayName human readable name for the commodity. + * @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of + * this commodity. + */ +@CordaSerializable +data class Commodity(val commodityCode: String, + val displayName: String, + val defaultFractionDigits: Int = 0) : TokenizableAssetInfo { + override val displayTokenSize: BigDecimal + get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits) + + companion object { + private val registry = mapOf( + // Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp + Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice")) + ) + + fun getInstance(commodityCode: String): Commodity? + = registry[commodityCode] + } +} + +/** + * Interface representing an agreement that exposes various attributes that are common. Implementing it simplifies + * implementation of general flows that manipulate many agreement types. + */ +interface DealState : LinearState { + /** Human readable well known reference (e.g. trade reference) */ + val ref: String + + /** + * Generate a partial transaction representing an agreement (command) to this deal, allowing a general + * deal/agreement flow to generate the necessary transaction for potential implementations. + * + * TODO: Currently this is the "inception" transaction but in future an offer of some description might be an input state ref + * + * TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a + * Contract instance from a ContractState are imminent, at which point we can move this out of here. + */ + fun generateAgreement(notary: Party): TransactionBuilder +} + +// TODO: Remove this from the interface +// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf()))")) +inline fun VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType().values.filter { + it.state.data.participants.any { it == party } +} + +/** + * Interface adding fixing specific methods. + */ +interface FixableDealState : DealState { + /** + * When is the next fixing and what is the fixing for? + */ + fun nextFixingOf(): FixOf? + + /** + * What oracle service to use for the fixing + */ + val oracleType: ServiceType + + /** + * Generate a fixing command for this deal and fix. + * + * TODO: This would also likely move to methods on the Contract once the changes to reference + * the Contract from the ContractState are in. + */ + fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix) +} + + +/** + * Interface for state objects that support being netted with other state objects. + */ +interface BilateralNettableState> { + /** + * Returns an object used to determine if two states can be subject to close-out netting. If two states return + * equal objects, they can be close out netted together. + */ + val bilateralNetState: Any + + /** + * Perform bilateral netting of this state with another state. The two states must be compatible (as in + * bilateralNetState objects are equal). + */ + fun net(other: N): N +} + +/** + * Interface for state objects that support being netted with other state objects. + */ +interface MultilateralNettableState { + /** + * Returns an object used to determine if two states can be subject to close-out netting. If two states return + * equal objects, they can be close out netted together. + */ + val multilateralNetState: T +} + +interface NettableState, out T : Any> : BilateralNettableState, + MultilateralNettableState 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 9e273dc09a..1d173fa5f6 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Cash.kt @@ -55,7 +55,9 @@ class Cash : OnLedgerAsset() { * to evolve without requiring code changes. But creates a risk that users create objects governed by a program * that is inconsistent with the legal contract. */ + // DOCSTART 2 override val legalContractReference: SecureHash = SecureHash.sha256("https://www.big-book-of-banking-law.gov/cash-claims.html") + // DOCEND 2 override fun extractCommands(commands: Collection>): List> = commands.select() @@ -82,6 +84,7 @@ class Cash : OnLedgerAsset() { class ConserveAmount : AbstractConserveAmount() } + // DOCSTART 1 /** A state representing a cash claim against some party. */ data class State( override val amount: Amount>, @@ -120,6 +123,7 @@ class Cash : OnLedgerAsset() { /** Object Relational Mapping support. */ override fun supportedSchemas(): Iterable = listOf(CashSchemaV1) } + // DOCEND 1 // Just for grouping interface Commands : FungibleAsset.Commands { @@ -206,7 +210,7 @@ infix fun Cash.State.`with deposit`(deposit: PartyAndReference): Cash.State = wi /** A randomly generated key. */ 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) } +val DUMMY_CASH_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=GB"), 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)), NULL_PARTY) /** An extension property that lets you get a cash state from an issued token, under the [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 c8cadda2ef..efa22e2264 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/CommodityContract.kt @@ -1,5 +1,6 @@ package net.corda.contracts.asset +import net.corda.contracts.Commodity import net.corda.contracts.clause.AbstractConserveAmount import net.corda.contracts.clause.AbstractIssue import net.corda.contracts.clause.NoZeroSizedOutputs 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 9cb6508363..a71f948fa9 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/Obligation.kt @@ -1,6 +1,9 @@ package net.corda.contracts.asset import com.google.common.annotations.VisibleForTesting +import net.corda.contracts.NetCommand +import net.corda.contracts.NetType +import net.corda.contracts.NettableState import net.corda.contracts.asset.Obligation.Lifecycle.NORMAL import net.corda.contracts.clause.* import net.corda.core.contracts.* @@ -24,36 +27,9 @@ 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() @@ -432,12 +408,12 @@ class Obligation

: Contract { if (input is State

) { val actualOutput = outputs[stateIdx] val deadline = input.dueBefore - val timestamp = tx.timestamp + val timeWindow = tx.timeWindow val expectedOutput = input.copy(lifecycle = expectedOutputLifecycle) requireThat { - "there is a timestamp from the authority" using (timestamp != null) - "the due date has passed" using (timestamp!!.after?.isAfter(deadline) ?: false) + "there is a time-window from the authority" using (timeWindow != null) + "the due date has passed" using (timeWindow!!.fromTime?.isAfter(deadline) ?: false) "input state lifecycle is correct" using (input.lifecycle == expectedInputLifecycle) "output state corresponds exactly to input state, with lifecycle changed" using (expectedOutput == actualOutput) } @@ -482,11 +458,11 @@ class Obligation

: Contract { * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + * @return the public keys who must sign the transaction for it to be valid. */ @Suppress("unused") fun generateExit(tx: TransactionBuilder, amountIssued: Amount>>, - assetStates: List>>): PublicKey + assetStates: List>>): Set = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState = { state, amount, owner -> state.copy(data = state.data.move(amount, owner)) }, generateMoveCommand = { -> Commands.Move() }, @@ -567,7 +543,7 @@ class Obligation

: Contract { } tx.addCommand(Commands.SetLifecycle(lifecycle), partiesUsed.map { it.owningKey }.distinct()) } - tx.setTime(issuanceDef.dueBefore, issuanceDef.timeTolerance) + tx.addTimeWindow(issuanceDef.dueBefore, issuanceDef.timeTolerance) } /** @@ -750,7 +726,7 @@ infix fun Obligation.State.`issued by`(party: AbstractParty) = copy /** A randomly generated key. */ val DUMMY_OBLIGATION_ISSUER_KEY by lazy { entropyToKeyPair(BigInteger.valueOf(10)) } /** A dummy, randomly generated issuer party by the name of "Snake Oil Issuer" */ -val DUMMY_OBLIGATION_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=UK"), DUMMY_OBLIGATION_ISSUER_KEY.public) } +val DUMMY_OBLIGATION_ISSUER by lazy { Party(X500Name("CN=Snake Oil Issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_OBLIGATION_ISSUER_KEY.public) } val Issued.OBLIGATION_DEF: Obligation.Terms get() = Obligation.Terms(nonEmptySetOf(Cash().legalContractReference), nonEmptySetOf(this), TEST_TX_TIME) 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 175182d48a..a6d3bd4185 100644 --- a/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt +++ b/finance/src/main/kotlin/net/corda/contracts/asset/OnLedgerAsset.kt @@ -159,7 +159,7 @@ abstract class OnLedgerAsset> : C * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not attempt to exit funds held by others. - * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + * @return the public keys which must sign the transaction for it to be valid. */ @Throws(InsufficientBalanceException::class) @JvmStatic @@ -167,7 +167,7 @@ abstract class OnLedgerAsset> : C assetStates: List>, deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): PublicKey { + generateExitCommand: (Amount>) -> CommandData): Set { val owner = assetStates.map { it.state.data.owner }.toSet().singleOrNull() ?: throw InsufficientBalanceException(amountIssued) val currency = amountIssued.token.product val amount = Amount(amountIssued.quantity, currency) @@ -193,9 +193,11 @@ 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.owningKey }) - tx.addCommand(generateExitCommand(amountIssued), gathered.flatMap { it.state.data.exitKeys }) - return amountIssued.token.issuer.party.owningKey + val moveKeys = gathered.map { it.state.data.owner.owningKey } + val exitKeys = gathered.flatMap { it.state.data.exitKeys } + tx.addCommand(generateMoveCommand(), moveKeys) + tx.addCommand(generateExitCommand(amountIssued), exitKeys) + return (moveKeys + exitKeys).toSet() } /** @@ -226,11 +228,11 @@ abstract class OnLedgerAsset> : C * necessarily owned by us. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not exit funds held by others. - * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + * @return the public keys which must sign the transaction for it to be valid. */ @Throws(InsufficientBalanceException::class) fun generateExit(tx: TransactionBuilder, amountIssued: Amount>, - assetStates: List>): PublicKey { + assetStates: List>): Set { return generateExit( tx, amountIssued, 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 c48386cd1e..f2fa484632 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/AbstractConserveAmount.kt @@ -26,7 +26,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : * @param amountIssued the amount to be exited, represented as a quantity of issued currency. * @param assetStates the asset states to take funds from. No checks are done about ownership of these states, it is * the responsibility of the caller to check that they do not attempt to exit funds held by others. - * @return the public key of the assets issuer, who must sign the transaction for it to be valid. + * @return the public keys which must sign the transaction for it to be valid. */ @Deprecated("This function will be removed in a future milestone", ReplaceWith("OnLedgerAsset.generateExit()")) @Throws(InsufficientBalanceException::class) @@ -34,7 +34,7 @@ abstract class AbstractConserveAmount, C : CommandData, T : assetStates: List>, deriveState: (TransactionState, Amount>, AbstractParty) -> TransactionState, generateMoveCommand: () -> CommandData, - generateExitCommand: (Amount>) -> CommandData): PublicKey + generateExitCommand: (Amount>) -> CommandData): Set = OnLedgerAsset.generateExit(tx, amountIssued, assetStates, deriveState, generateMoveCommand, generateExitCommand) override fun verify(tx: TransactionForContract, 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 5eee72380e..5c791b1a84 100644 --- a/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt +++ b/finance/src/main/kotlin/net/corda/contracts/clause/Net.kt @@ -1,6 +1,8 @@ package net.corda.contracts.clause import com.google.common.annotations.VisibleForTesting +import net.corda.contracts.NetCommand +import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation import net.corda.contracts.asset.extractAmountsDue import net.corda.contracts.asset.sumAmountsDue diff --git a/core/src/main/kotlin/net/corda/core/math/Interpolators.kt b/finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt similarity index 95% rename from core/src/main/kotlin/net/corda/core/math/Interpolators.kt rename to finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt index d8ae4cfe2e..62fc40a4c6 100644 --- a/core/src/main/kotlin/net/corda/core/math/Interpolators.kt +++ b/finance/src/main/kotlin/net/corda/contracts/math/Interpolators.kt @@ -1,4 +1,4 @@ -package net.corda.core.math +package net.corda.contracts.math import java.util.* @@ -37,7 +37,9 @@ class LinearInterpolator(private val xs: DoubleArray, private val ys: DoubleArra } private fun interpolateBetween(x: Double, x1: Double, x2: Double, y1: Double, y2: Double): Double { - return y1 + (y2 - y1) * (x - x1) / (x2 - x1) + // N.B. The classic y1 + (y2 - y1) * (x - x1) / (x2 - x1) is numerically unstable!! + val deltaX = (x - x1) / (x2 - x1) + return y1 * (1.0 - deltaX) + y2 * deltaX } } 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 5e21ed316b..5158b85eb4 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/DummyDealContract.kt @@ -1,12 +1,12 @@ package net.corda.contracts.testing +import net.corda.contracts.DealState import net.corda.core.contracts.Contract -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.crypto.SecureHash +import net.corda.core.crypto.containsAny import net.corda.core.identity.AbstractParty -import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party import net.corda.core.transactions.TransactionBuilder import java.security.PublicKey @@ -20,8 +20,7 @@ class DummyDealContract : Contract { override val contract: Contract = DummyDealContract(), override val participants: List = listOf(), override val linearId: UniqueIdentifier = UniqueIdentifier(), - override val ref: String, - override val parties: List = listOf()) : DealState { + override val ref: String) : DealState { override fun isRelevant(ourKeys: Set): Boolean { return participants.any { it.owningKey.containsAny(ourKeys) } } 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 926b3e7087..d935646ca0 100644 --- a/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt +++ b/finance/src/main/kotlin/net/corda/contracts/testing/VaultFiller.kt @@ -2,6 +2,7 @@ package net.corda.contracts.testing +import net.corda.contracts.DealState import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.asset.DUMMY_CASH_ISSUER_KEY @@ -21,7 +22,6 @@ import java.util.* @JvmOverloads fun ServiceHub.fillWithSomeTestDeals(dealIds: List, - revisions: Int? = 0, participants: List = emptyList()) : Vault { val freshKey = keyManagementService.freshKey() val recipient = AnonymousParty(freshKey) @@ -99,7 +99,7 @@ fun ServiceHub.fillWithSomeTestCash(howMuch: Amount, // 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) + val issuance = TransactionType.General.Builder(null as Party?) cash.generateIssue(issuance, Amount(pennies, Issued(issuedBy.copy(reference = ref), howMuch.token)), me, outputNotary) issuance.signWith(issuerKey) diff --git a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt index 734692f751..7a9e441668 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashExitFlow.kt @@ -33,10 +33,10 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog @Throws(CashException::class) override fun call(): SignedTransaction { progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionType.General.Builder(null) + val builder: TransactionBuilder = TransactionType.General.Builder(notary = null as Party?) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) val exitStates = serviceHub.vaultService.unconsumedStatesForSpending(amount, setOf(issuer.party), builder.notary, builder.lockId, setOf(issuer.reference)) - try { + val signers = try { Cash().generateExit( builder, amount.issuedBy(issuer), @@ -62,7 +62,7 @@ class CashExitFlow(val amount: Amount, val issueRef: OpaqueBytes, prog .toSet() // Sign transaction progressTracker.currentStep = SIGNING_TX - val tx = serviceHub.signInitialTransaction(builder) + val tx = serviceHub.signInitialTransaction(builder, signers) // Commit the transaction progressTracker.currentStep = FINALISING_TX diff --git a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt index d06177783f..479e1a1d6d 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashIssueFlow.kt @@ -35,7 +35,7 @@ class CashIssueFlow(val amount: Amount, @Suspendable override fun call(): SignedTransaction { progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionType.General.Builder(notary = null) + val builder: TransactionBuilder = TransactionType.General.Builder(notary = notary) val issuer = serviceHub.myInfo.legalIdentity.ref(issueRef) // TODO: Get a transaction key, don't just re-use the owning key Cash().generateIssue(builder, amount.issuedBy(issuer), recipient, notary) diff --git a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt index 1e274ac1bb..b7642d24c1 100644 --- a/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/CashPaymentFlow.kt @@ -30,7 +30,7 @@ open class CashPaymentFlow( @Suspendable override fun call(): SignedTransaction { progressTracker.currentStep = GENERATING_TX - val builder: TransactionBuilder = TransactionType.General.Builder(null) + val builder: TransactionBuilder = TransactionType.General.Builder(null as Party?) // TODO: Have some way of restricting this to states the caller controls val (spendTX, keysForSigning) = try { serviceHub.vaultService.generateSpend( diff --git a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt index ab6b4a8883..50bfe72327 100644 --- a/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/IssuerFlow.kt @@ -2,12 +2,8 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.* -import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowLogic -import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.* 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 import net.corda.core.transactions.SignedTransaction @@ -46,6 +42,7 @@ object IssuerFlow { * Issuer refers to a Node acting as a Bank Issuer of [FungibleAsset], and processes requests from a [IssuanceRequester] client. * Returns the generated transaction representing the transfer of the [Issued] [FungibleAsset] to the issue requester. */ + @InitiatedBy(IssuanceRequester::class) class Issuer(val otherParty: Party) : FlowLogic() { companion object { object AWAITING_REQUEST : ProgressTracker.Step("Awaiting issuance request") @@ -97,11 +94,5 @@ object IssuerFlow { // NOTE: CashFlow PayCash calls FinalityFlow which performs a Broadcast (which stores a local copy of the txn to the ledger) return moveTx } - - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(IssuanceRequester::class.java, ::Issuer) - } - } } } \ No newline at end of file diff --git a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt similarity index 94% rename from core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt rename to finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt index c776be45e8..d9ebadad53 100644 --- a/core/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyDealFlow.kt @@ -1,10 +1,9 @@ package net.corda.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.DealState +import net.corda.contracts.DealState 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 @@ -100,7 +99,10 @@ object TwoPartyDealFlow { logger.trace { "Signed proposed transaction." } progressTracker.currentStep = COLLECTING_SIGNATURES + + // DOCSTART 1 val stx = subFlow(CollectSignaturesFlow(ptx)) + // DOCEND 1 logger.trace { "Got signatures from other party, verifying ... " } @@ -181,10 +183,10 @@ object TwoPartyDealFlow { val deal = handshake.payload.dealBeingOffered val ptx = deal.generateAgreement(handshake.payload.notary) - // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt - // to have one. - ptx.setTime(serviceHub.clock.instant(), 30.seconds) - return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) + // And add a request for a time-window: it may be that none of the contracts need this! + // But it can't hurt to have one. + ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) + return Pair(ptx, arrayListOf(deal.participants.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) } } } diff --git a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt index 3f02121d19..bb7f4eda86 100644 --- a/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt +++ b/finance/src/main/kotlin/net/corda/flows/TwoPartyTradeFlow.kt @@ -3,7 +3,6 @@ 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.DigitalSignature import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.identity.AnonymousParty @@ -13,9 +12,7 @@ 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.trace import net.corda.core.utilities.unwrap import java.security.PublicKey import java.util.* @@ -36,8 +33,6 @@ import java.util.* * 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 @@ -57,10 +52,6 @@ object TwoPartyTradeFlow { val sellerOwnerKey: PublicKey ) - @CordaSerializable - data class SignaturesFromSeller(val sellerSig: DigitalSignature.WithKey, - val notarySig: DigitalSignature.WithKey) - open class Seller(val otherParty: Party, val notaryNode: NodeInfo, val assetToSell: StateAndRef, @@ -70,62 +61,42 @@ object TwoPartyTradeFlow { companion object { object AWAITING_PROPOSAL : ProgressTracker.Step("Awaiting transaction proposal") - object VERIFYING : ProgressTracker.Step("Verifying transaction proposal") - object SIGNING : ProgressTracker.Step("Signing transaction") // DOCSTART 3 - object COMMITTING : ProgressTracker.Step("Committing transaction to the ledger") { - override fun childProgressTracker() = FinalityFlow.tracker() + object VERIFYING_AND_SIGNING : ProgressTracker.Step("Verifying and signing transaction proposal") { + override fun childProgressTracker() = SignTransactionFlow.tracker() } - // DOCEND 3 - object SENDING_FINAL_TX : ProgressTracker.Step("Sending final transaction to buyer") - fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, COMMITTING, SENDING_FINAL_TX) + fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING_AND_SIGNING) } // DOCSTART 4 @Suspendable override fun call(): SignedTransaction { - val partialSTX: SignedTransaction = receiveAndCheckProposedTransaction() - val ourSignature = calculateOurSignature(partialSTX) - val unnotarisedSTX: SignedTransaction = partialSTX + ourSignature - val finishedSTX = subFlow(FinalityFlow(unnotarisedSTX)).single() - return finishedSTX - } - // DOCEND 4 - - // DOCSTART 5 - @Suspendable - private fun receiveAndCheckProposedTransaction(): SignedTransaction { progressTracker.currentStep = AWAITING_PROPOSAL - // Make the first message we'll send to kick off the flow. 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) + send(otherParty, hello) - progressTracker.currentStep = VERIFYING - return untrustedSTX.unwrap { - // Check that the tx proposed by the buyer is valid. - 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(AnonymousParty(myKey)).withoutIssuer() != price) - throw FlowException("Transaction is not sending us the right amount of cash") - - it + // Verify and sign the transaction. + progressTracker.currentStep = VERIFYING_AND_SIGNING + // DOCSTART 5 + val signTransactionFlow = object : SignTransactionFlow(otherParty, VERIFYING_AND_SIGNING.childProgressTracker()) { + override fun checkTransaction(stx: SignedTransaction) { + if (stx.tx.outputs.map { it.data }.sumCashBy(AnonymousParty(myKey)).withoutIssuer() != price) + throw FlowException("Transaction is not sending us the right amount of cash") + } } + return subFlow(signTransactionFlow) + // DOCEND 5 } - // DOCEND 5 + // DOCEND 4 // Following comment moved here so that it doesn't appear in the docsite: - // There are all sorts of funny games a malicious secondary might play with it sends maybeSTX (in - // receiveAndCheckProposedTransaction), we should fix them: + // There are all sorts of funny games a malicious secondary might play with it sends maybeSTX, + // 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! @@ -134,11 +105,6 @@ object TwoPartyTradeFlow { // // 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. - - open fun calculateOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey { - progressTracker.currentStep = SIGNING - return serviceHub.createSignature(partialTX, myKey) - } } open class Buyer(val otherParty: Party, @@ -149,10 +115,15 @@ object TwoPartyTradeFlow { object RECEIVING : ProgressTracker.Step("Waiting for seller trading info") object VERIFYING : ProgressTracker.Step("Verifying seller assets") object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal") - object SENDING_SIGNATURES : ProgressTracker.Step("Sending signatures to the seller") - object WAITING_FOR_TX : ProgressTracker.Step("Waiting for the transaction to finalise.") + object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties") { + override fun childProgressTracker() = CollectSignaturesFlow.tracker() + } + object RECORDING : ProgressTracker.Step("Recording completed transaction") { + // TODO: Currently triggers a race condition on Team City. See https://github.com/corda/corda/issues/733. + // override fun childProgressTracker() = FinalityFlow.tracker() + } - override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SENDING_SIGNATURES, WAITING_FOR_TX) + override val progressTracker = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING) // DOCEND 2 // DOCSTART 1 @@ -165,16 +136,16 @@ object TwoPartyTradeFlow { // Put together a proposed transaction that performs the trade, and sign it. progressTracker.currentStep = SIGNING val (ptx, cashSigningPubKeys) = assembleSharedTX(tradeRequest) - val stx = signWithOurKeys(cashSigningPubKeys, ptx) + val partSignedTx = signWithOurKeys(cashSigningPubKeys, ptx) // Send the signed transaction to the seller, who must then sign it themselves and commit // it to the ledger by sending it to the notary. - progressTracker.currentStep = SENDING_SIGNATURES - send(otherParty, stx) + progressTracker.currentStep = COLLECTING_SIGNATURES + val twiceSignedTx = subFlow(CollectSignaturesFlow(partSignedTx, COLLECTING_SIGNATURES.childProgressTracker())) - // Wait for the finished, notarised transaction to arrive in our transaction store. - progressTracker.currentStep = WAITING_FOR_TX - return waitForLedgerCommit(stx.id) + // Notarise and record the transaction. + progressTracker.currentStep = RECORDING + return subFlow(FinalityFlow(twiceSignedTx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single() } @Suspendable @@ -186,7 +157,6 @@ object TwoPartyTradeFlow { // What is the seller trying to sell us? val asset = it.assetForSale.state.data val assetTypeName = asset.javaClass.name - logger.trace { "Got trade request for a $assetTypeName: ${it.assetForSale}" } if (it.price > acceptablePrice) throw UnacceptablePriceException(it.price) @@ -225,10 +195,10 @@ object TwoPartyTradeFlow { tx.addOutputState(state, tradeRequest.assetForSale.state.notary) 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. + // And add a request for a time-window: it may be that none of the contracts need this! + // But it can't hurt to have one. val currentTime = serviceHub.clock.instant() - tx.setTime(currentTime, 30.seconds) + tx.addTimeWindow(currentTime, 30.seconds) return Pair(tx, cashSigningPubKeys) } // DOCEND 1 diff --git a/core/src/main/resources/net/corda/core/contracts/LondonHolidayCalendar.txt b/finance/src/main/resources/net/corda/contracts/LondonHolidayCalendar.txt similarity index 100% rename from core/src/main/resources/net/corda/core/contracts/LondonHolidayCalendar.txt rename to finance/src/main/resources/net/corda/contracts/LondonHolidayCalendar.txt diff --git a/core/src/main/resources/net/corda/core/contracts/NewYorkHolidayCalendar.txt b/finance/src/main/resources/net/corda/contracts/NewYorkHolidayCalendar.txt similarity index 100% rename from core/src/main/resources/net/corda/core/contracts/NewYorkHolidayCalendar.txt rename to finance/src/main/resources/net/corda/contracts/NewYorkHolidayCalendar.txt diff --git a/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java b/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java index 657f80059d..9c8d3086e3 100644 --- a/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java +++ b/finance/src/test/java/net/corda/flows/AbstractStateReplacementFlowTest.java @@ -1,5 +1,6 @@ package net.corda.flows; +import net.corda.core.identity.Party; import net.corda.core.identity.Party; import net.corda.core.utilities.*; import org.jetbrains.annotations.*; @@ -17,4 +18,4 @@ public class AbstractStateReplacementFlowTest { protected void verifyProposal(@NotNull AbstractStateReplacementFlow.Proposal proposal) { } } -} \ No newline at end of file +} diff --git a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt index 11f47d4161..4b9c409a85 100644 --- a/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/CommercialPaperTests.kt @@ -97,7 +97,7 @@ class CommercialPaperTestsGeneric { transaction("Issuance") { output("paper") { thisTest.getPaper() } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } @@ -129,17 +129,17 @@ class CommercialPaperTestsGeneric { tweak { outputs(700.DOLLARS `issued by` issuer) - timestamp(TEST_TX_TIME + 8.days) + timeWindow(TEST_TX_TIME + 8.days) this `fails with` "received amount equals the face value" } outputs(1000.DOLLARS `issued by` issuer) tweak { - timestamp(TEST_TX_TIME + 2.days) + timeWindow(TEST_TX_TIME + 2.days) this `fails with` "must have matured" } - timestamp(TEST_TX_TIME + 8.days) + timeWindow(TEST_TX_TIME + 8.days) tweak { output { "paper".output() } @@ -156,7 +156,7 @@ class CommercialPaperTestsGeneric { transaction { output { thisTest.getPaper() } command(DUMMY_PUBKEY_1) { thisTest.getIssueCommand(DUMMY_NOTARY) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "output states are issued by a command signer" } } @@ -166,7 +166,7 @@ class CommercialPaperTestsGeneric { transaction { output { thisTest.getPaper().withFaceValue(0.DOLLARS `issued by` issuer) } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } } @@ -176,7 +176,7 @@ class CommercialPaperTestsGeneric { transaction { output { thisTest.getPaper().withMaturityDate(TEST_TX_TIME - 10.days) } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "maturity date is not in the past" } } @@ -187,7 +187,7 @@ class CommercialPaperTestsGeneric { input(thisTest.getPaper()) output { thisTest.getPaper() } command(MEGA_CORP_PUBKEY) { thisTest.getIssueCommand(DUMMY_NOTARY) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "output values sum to more than the inputs" } } @@ -259,7 +259,7 @@ class CommercialPaperTestsGeneric { val issuance = bigCorpServices.myInfo.legalIdentity.ref(1) val issueTX: SignedTransaction = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) + addTimeWindow(TEST_TX_TIME, 30.seconds) signWith(bigCorpServices.key) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() @@ -289,7 +289,7 @@ class CommercialPaperTestsGeneric { databaseBigCorp.transaction { fun makeRedeemTX(time: Instant): Pair { val ptx = TransactionType.General.Builder(DUMMY_NOTARY) - ptx.setTime(time, 30.seconds) + ptx.addTimeWindow(time, 30.seconds) CommercialPaper().generateRedeem(ptx, moveTX.tx.outRef(1), bigCorpVaultService) ptx.signWith(aliceServices.key) ptx.signWith(bigCorpServices.key) diff --git a/core/src/test/kotlin/net/corda/core/FinanceTypesTest.kt b/finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt similarity index 78% rename from core/src/test/kotlin/net/corda/core/FinanceTypesTest.kt rename to finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt index 9f21d21cfb..8e9618b035 100644 --- a/core/src/test/kotlin/net/corda/core/FinanceTypesTest.kt +++ b/finance/src/test/kotlin/net/corda/contracts/FinanceTypesTest.kt @@ -1,19 +1,14 @@ -package net.corda.core +package net.corda.contracts -import net.corda.core.contracts.* import org.junit.Test import java.time.LocalDate -import java.util.* -import kotlin.test.* +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue class FinanceTypesTest { - @Test - fun `make sure Amount has decimal places`() { - val x = Amount(1, Currency.getInstance("USD")) - assertTrue("0.01" in x.toString()) - } - @Test fun `valid tenor tests`() { val exampleTenors = ("ON,1D,2D,3D,4D,5D,6D,7D,1W,2W,3W,1M,3M,6M,1Y,2Y,3Y,5Y,10Y,12Y,20Y").split(",") @@ -150,32 +145,4 @@ class FinanceTypesTest { } } - - @Test - fun `unique identifier comparison`() { - val ids = listOf(UniqueIdentifier.fromString("e363f00e-4759-494d-a7ca-0dc966a92494"), - UniqueIdentifier.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d"), - UniqueIdentifier("Test", UUID.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d")) - ) - assertEquals(-1, ids[0].compareTo(ids[1])) - assertEquals(1, ids[1].compareTo(ids[0])) - assertEquals(0, ids[0].compareTo(ids[0])) - // External ID is not taken into account - assertEquals(0, ids[1].compareTo(ids[2])) - } - - @Test - fun `unique identifier equality`() { - val ids = listOf(UniqueIdentifier.fromString("e363f00e-4759-494d-a7ca-0dc966a92494"), - UniqueIdentifier.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d"), - UniqueIdentifier("Test", UUID.fromString("10ed0cc3-7bdf-4000-b610-595e36667d7d")) - ) - assertEquals(ids[0], ids[0]) - assertNotEquals(ids[0], ids[1]) - assertEquals(ids[0].hashCode(), ids[0].hashCode()) - assertNotEquals(ids[0].hashCode(), ids[1].hashCode()) - // External ID is not taken into account - assertEquals(ids[1], ids[2]) - assertEquals(ids[1].hashCode(), ids[2].hashCode()) - } } 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 bbb6e1aaf1..637d5d62a5 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/CashTests.kt @@ -58,7 +58,7 @@ class CashTests { database = dataSourceAndDatabase.second database.transaction { services = object : MockServices() { - override val keyManagementService: MockKeyManagementService = MockKeyManagementService(MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) + override val keyManagementService: MockKeyManagementService = MockKeyManagementService(identityService, MINI_CORP_KEY, MEGA_CORP_KEY, OUR_KEY) override val vaultService: VaultService = makeVaultService(dataSourceProps) override fun recordTransactions(txs: Iterable) { 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 c0333a2c26..58edc73e78 100644 --- a/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt +++ b/finance/src/test/kotlin/net/corda/contracts/asset/ObligationTests.kt @@ -1,5 +1,7 @@ package net.corda.contracts.asset +import net.corda.contracts.Commodity +import net.corda.contracts.NetType import net.corda.contracts.asset.Obligation.Lifecycle import net.corda.core.contracts.* import net.corda.core.crypto.NULL_PARTY @@ -341,7 +343,7 @@ class ObligationTests { input("Bob's $1,000,000 obligation to Alice") // Note we can sign with either key here command(ALICE_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } this.verifies() @@ -357,7 +359,7 @@ class ObligationTests { input("MegaCorp's $1,000,000 obligation to Bob") 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) + timeWindow(TEST_TX_TIME) this.verifies() } this.verifies() @@ -371,7 +373,7 @@ class ObligationTests { input("Bob's $1,000,000 obligation to Alice") output("change") { (oneMillionDollars.splitEvenly(2).first()).OBLIGATION between Pair(ALICE, BOB) } command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "amounts owed on input and output must match" } } @@ -383,7 +385,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") command(MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.CLOSE_OUT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "any involved party has signed" } } @@ -398,7 +400,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } this.verifies() @@ -412,7 +414,7 @@ class ObligationTests { input("Alice's $1,000,000 obligation to Bob") input("Bob's $1,000,000 obligation to Alice") command(BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "all involved parties have signed" } } @@ -425,7 +427,7 @@ class ObligationTests { 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) } command(ALICE_PUBKEY, BOB_PUBKEY, MEGA_CORP_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } this.verifies() @@ -439,7 +441,7 @@ class ObligationTests { 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) } command(ALICE_PUBKEY, BOB_PUBKEY) { Obligation.Commands.Net(NetType.PAYMENT) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "all involved parties have signed" } } @@ -503,7 +505,7 @@ class ObligationTests { @Test fun `commodity settlement`() { - val defaultFcoj = FCOJ `issued by` defaultIssuer + val defaultFcoj = Issued(defaultIssuer, Commodity.getInstance("FCOJ")!!) 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, @@ -527,14 +529,14 @@ class ObligationTests { @Test fun `payment default`() { - // Try defaulting an obligation without a timestamp + // Try defaulting an obligation without a time-window. ledger { 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)).copy(lifecycle = Lifecycle.DEFAULTED) } command(BOB_PUBKEY) { Obligation.Commands.SetLifecycle(Lifecycle.DEFAULTED) } - this `fails with` "there is a timestamp from the authority" + this `fails with` "there is a time-window from the authority" } } @@ -545,7 +547,7 @@ class ObligationTests { 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) + timeWindow(TEST_TX_TIME) this `fails with` "the due date has passed" } @@ -555,7 +557,7 @@ class ObligationTests { 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) + timeWindow(TEST_TX_TIME) this.verifies() } this.verifies() diff --git a/core/src/test/kotlin/net/corda/core/math/InterpolatorsTest.kt b/finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt similarity index 98% rename from core/src/test/kotlin/net/corda/core/math/InterpolatorsTest.kt rename to finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt index 4a5dec1c40..1041a4507d 100644 --- a/core/src/test/kotlin/net/corda/core/math/InterpolatorsTest.kt +++ b/finance/src/test/kotlin/net/corda/contracts/math/InterpolatorsTest.kt @@ -1,4 +1,4 @@ -package net.corda.core.math +package net.corda.contracts.math import org.junit.Assert import org.junit.Test 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 1b9789bc04..fe65daa626 100644 --- a/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt +++ b/finance/src/test/kotlin/net/corda/contracts/testing/Generators.kt @@ -75,7 +75,7 @@ class WiredTransactionGenerator : Generator(WireTransaction::cl notary = PartyGenerator().generate(random, status), signers = commands.flatMap { it.signers }, type = TransactionType.General, - timestamp = TimestampGenerator().generate(random, status) + timeWindow = TimeWindowGenerator().generate(random, status) ) } } diff --git a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt index 5754efd8a5..9a09229162 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashExitFlowTests.kt @@ -16,7 +16,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashExitFlowTests { - private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: MockNode @@ -26,23 +26,23 @@ class CashExitFlowTests { @Before fun start() { - val nodes = net.createTwoNodes() + val nodes = mockNet.createTwoNodes() notaryNode = nodes.first bankOfCordaNode = nodes.second notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - net.runNetwork() + mockNet.runNetwork() val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, bankOfCorda, notary)).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() } @After fun cleanUp() { - net.stopNodes() + mockNet.stopNodes() } @Test @@ -50,7 +50,7 @@ class CashExitFlowTests { val exitAmount = 500.DOLLARS val future = bankOfCordaNode.services.startFlow(CashExitFlow(exitAmount, ref)).resultFuture - net.runNetwork() + mockNet.runNetwork() val exitTx = future.getOrThrow().tx val expected = (initialBalance - exitAmount).`issued by`(bankOfCorda.ref(ref)) assertEquals(1, exitTx.inputs.size) @@ -64,7 +64,7 @@ class CashExitFlowTests { val expected = 0.DOLLARS val future = bankOfCordaNode.services.startFlow(CashExitFlow(expected, ref)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith { future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt index add01685be..5f81c03e40 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashIssueFlowTests.kt @@ -16,7 +16,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashIssueFlowTests { - private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) private lateinit var bankOfCordaNode: MockNode private lateinit var bankOfCorda: Party private lateinit var notaryNode: MockNode @@ -24,18 +24,18 @@ class CashIssueFlowTests { @Before fun start() { - val nodes = net.createTwoNodes() + val nodes = mockNet.createTwoNodes() notaryNode = nodes.first bankOfCordaNode = nodes.second notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - net.runNetwork() + mockNet.runNetwork() } @After fun cleanUp() { - net.stopNodes() + mockNet.stopNodes() } @Test @@ -45,7 +45,7 @@ class CashIssueFlowTests { val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, ref, bankOfCorda, notary)).resultFuture - net.runNetwork() + mockNet.runNetwork() val issueTx = future.getOrThrow() val output = issueTx.tx.outputs.single().data as Cash.State assertEquals(expected.`issued by`(bankOfCorda.ref(ref)), output.amount) @@ -57,7 +57,7 @@ class CashIssueFlowTests { val future = bankOfCordaNode.services.startFlow(CashIssueFlow(expected, OpaqueBytes.of(0x01), bankOfCorda, notary)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith { future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt index 33f15b0898..58779cf1b7 100644 --- a/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt +++ b/finance/src/test/kotlin/net/corda/flows/CashPaymentFlowTests.kt @@ -3,8 +3,8 @@ package net.corda.flows import net.corda.contracts.asset.Cash import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.`issued by` -import net.corda.core.identity.Party import net.corda.core.getOrThrow +import net.corda.core.identity.Party import net.corda.core.serialization.OpaqueBytes import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork @@ -16,7 +16,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class CashPaymentFlowTests { - private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) private val initialBalance = 2000.DOLLARS private val ref = OpaqueBytes.of(0x01) private lateinit var bankOfCordaNode: MockNode @@ -26,37 +26,39 @@ class CashPaymentFlowTests { @Before fun start() { - val nodes = net.createTwoNodes() + val nodes = mockNet.createTwoNodes() notaryNode = nodes.first bankOfCordaNode = nodes.second notary = notaryNode.info.notaryIdentity bankOfCorda = bankOfCordaNode.info.legalIdentity - net.runNetwork() + mockNet.runNetwork() val future = bankOfCordaNode.services.startFlow(CashIssueFlow(initialBalance, ref, bankOfCorda, notary)).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() } @After fun cleanUp() { - net.stopNodes() + mockNet.stopNodes() } @Test fun `pay some cash`() { val payTo = notaryNode.info.legalIdentity - val expected = 500.DOLLARS - val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, + val expectedPayment = 500.DOLLARS + val expectedChange = 1500.DOLLARS + val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expectedPayment, payTo)).resultFuture - net.runNetwork() + mockNet.runNetwork() val paymentTx = future.getOrThrow() val states = paymentTx.tx.outputs.map { it.data }.filterIsInstance() 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) + assertEquals(expectedChange.`issued by`(bankOfCorda.ref(ref)), ourState.amount) + assertEquals(expectedPayment.`issued by`(bankOfCorda.ref(ref)), paymentState.amount) } @Test @@ -65,7 +67,7 @@ class CashPaymentFlowTests { val expected = 4000.DOLLARS val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, payTo)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith { future.getOrThrow() } @@ -77,7 +79,7 @@ class CashPaymentFlowTests { val expected = 0.DOLLARS val future = bankOfCordaNode.services.startFlow(CashPaymentFlow(expected, payTo)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertFailsWith { future.getOrThrow() } diff --git a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt index 4721ca8aeb..dd812e30c2 100644 --- a/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt +++ b/finance/src/test/kotlin/net/corda/flows/IssuerFlowTest.kt @@ -6,108 +6,95 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.currency import net.corda.core.flows.FlowException -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.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.toFuture import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.IssuerFlow.IssuanceRequester import net.corda.testing.BOC import net.corda.testing.MEGA_CORP -import net.corda.testing.initiateSingleShotFlow -import net.corda.testing.ledger import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode +import org.junit.After +import org.junit.Before import org.junit.Test +import rx.Observable import java.util.* import kotlin.test.assertEquals import kotlin.test.assertFailsWith class IssuerFlowTest { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNode lateinit var bankOfCordaNode: MockNode lateinit var bankClientNode: MockNode + @Before + fun start() { + mockNet = MockNetwork(threadPerNode = true) + notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + bankOfCordaNode = mockNet.createPartyNode(notaryNode.info.address, BOC.name) + bankClientNode = mockNet.createPartyNode(notaryNode.info.address, MEGA_CORP.name) + } + + @After + fun cleanUp() { + mockNet.stopNodes() + } + @Test fun `test issuer flow`() { - net = MockNetwork(false, true) - ledger { - notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) - bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name) - bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name) + // using default IssueTo Party Reference + val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, 1000000.DOLLARS, + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) - // using default IssueTo Party Reference - 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")), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).issueRequestResult.getOrThrow() - } - - bankOfCordaNode.stop() - bankClientNode.stop() + // try to issue an amount of a restricted currency + assertFailsWith { + runIssuerAndIssueRequester(bankOfCordaNode, bankClientNode, Amount(100000L, currency("BRL")), + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)).issueRequestResult.getOrThrow() } } @Test fun `test issue flow to self`() { - net = MockNetwork(false, true) - ledger { - notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) - bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name) - - // using default IssueTo Party Reference - val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, - bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) - assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) - - bankOfCordaNode.stop() - } + // using default IssueTo Party Reference + val (issuer, issuerResult) = runIssuerAndIssueRequester(bankOfCordaNode, bankOfCordaNode, 1000000.DOLLARS, + bankOfCordaNode.info.legalIdentity, OpaqueBytes.of(123)) + assertEquals(issuerResult.get(), issuer.get().resultFuture.get()) } @Test fun `test concurrent issuer flow`() { - - net = MockNetwork(false, true) - ledger { - notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) - bankOfCordaNode = net.createPartyNode(notaryNode.info.address, BOC.name) - bankClientNode = net.createPartyNode(notaryNode.info.address, MEGA_CORP.name) - - // 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), - bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) - } - handles.forEach { - require(it.issueRequestResult.get() is SignedTransaction) - } - - bankOfCordaNode.stop() - bankClientNode.stop() + // 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), + bankClientNode.info.legalIdentity, OpaqueBytes.of(123)) + } + handles.forEach { + require(it.issueRequestResult.get() is SignedTransaction) } } - private fun runIssuerAndIssueRequester(issuerNode: MockNode, issueToNode: MockNode, + private fun runIssuerAndIssueRequester(issuerNode: MockNode, + issueToNode: MockNode, amount: Amount, - party: Party, ref: OpaqueBytes): RunResult { + party: Party, + ref: OpaqueBytes): RunResult { val issueToPartyAndRef = party.ref(ref) - val issuerFuture = issuerNode.initiateSingleShotFlow(IssuerFlow.IssuanceRequester::class) { _ -> - IssuerFlow.Issuer(party) - }.map { it.stateMachine } + val issuerFlows: Observable = issuerNode.registerInitiatedFlow(IssuerFlow.Issuer::class.java) + val firstIssuerFiber = issuerFlows.toFuture().map { it.stateMachine } val issueRequest = IssuanceRequester(amount, party, issueToPartyAndRef.reference, issuerNode.info.legalIdentity) val issueRequestResultFuture = issueToNode.services.startFlow(issueRequest).resultFuture - return IssuerFlowTest.RunResult(issuerFuture, issueRequestResultFuture) + return IssuerFlowTest.RunResult(firstIssuerFiber, issueRequestResultFuture) } private data class RunResult( diff --git a/gradle-plugins/cordformation/README.rst b/gradle-plugins/cordformation/README.rst index 77671f5a26..f619737a91 100644 --- a/gradle-plugins/cordformation/README.rst +++ b/gradle-plugins/cordformation/README.rst @@ -1 +1 @@ -Please refer to the documentation in /doc/build/html/creating-a-cordapp.html#cordformation. \ No newline at end of file +Please refer to the documentation in /doc/build/html/running-a-node.html#cordformation. \ No newline at end of file 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 0107b26e8b..4a9711572e 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 @@ -3,6 +3,7 @@ package net.corda.plugins import java.awt.GraphicsEnvironment import java.io.File import java.nio.file.Files +import java.nio.file.Path import java.util.* private val HEADLESS_FLAG = "--headless" @@ -64,20 +65,23 @@ private object WebJarType : JarType("corda-webserver.jar") { private abstract class JavaCommand(jarName: String, internal val dir: File, debugPort: Int?, internal val nodeName: String, init: MutableList.() -> Unit, args: List) { internal val command: List = mutableListOf().apply { - add(File(File(System.getProperty("java.home"), "bin"), "java").path) + add(getJavaPath()) add("-Dname=$nodeName") null != debugPort && add("-Dcapsule.jvm.args=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$debugPort") - add("-jar"); add(jarName) + add("-jar") + add(jarName) init() addAll(args) } internal abstract fun processBuilder(): ProcessBuilder internal fun start() = processBuilder().directory(dir).start() + internal abstract fun getJavaPath(): String } private class HeadlessJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List) : JavaCommand(jarName, dir, debugPort, dir.name, { add("--no-local-shell") }, args) { override fun processBuilder() = ProcessBuilder(command).redirectError(File("error.$nodeName.log")).inheritIO() + override fun getJavaPath() = File(File(System.getProperty("java.home"), "bin"), "java").path } private class TerminalWindowJavaCommand(jarName: String, dir: File, debugPort: Int?, args: List) : JavaCommand(jarName, dir, debugPort, "${dir.name}-$jarName", {}, args) { @@ -105,6 +109,12 @@ end tell""") }) private fun unixCommand() = command.map(::quotedFormOf).joinToString(" ") + override fun getJavaPath(): String { + val path = File(File(System.getProperty("java.home"), "bin"), "java").path + // Replace below is to fix an issue with spaces in paths on Windows. + // Quoting the entire path does not work, only the space or directory within the path. + return if(os == OS.WINDOWS) path.replace(" ", "\" \"") else path + } } private fun quotedFormOf(text: String) = "'${text.replace("'", "'\\''")}'" // Suitable for UNIX shells. diff --git a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy index 8dd3346432..f5221ff037 100644 --- a/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy +++ b/gradle-plugins/publish-utils/src/main/groovy/net/corda/plugins/PublishTasks.groovy @@ -60,11 +60,6 @@ class PublishTasks implements Plugin { void configureMavenPublish(BintrayConfigExtension bintrayConfig) { project.apply([plugin: 'maven-publish']) project.publishing.publications.create(publishName, MavenPublication) { - if(!publishConfig.disableDefaultJar && !publishConfig.publishWar) { - from project.components.java - } else if(publishConfig.publishWar) { - from project.components.web - } groupId project.group artifactId publishName @@ -76,6 +71,12 @@ class PublishTasks implements Plugin { delegate.artifact it } + if (!publishConfig.disableDefaultJar && !publishConfig.publishWar) { + from project.components.java + } else if (publishConfig.publishWar) { + from project.components.web + } + extendPomForMavenCentral(pom, bintrayConfig) } project.task("install", dependsOn: "publishToMavenLocal") diff --git a/gradle-plugins/settings.gradle b/gradle-plugins/settings.gradle index 061db29b3a..990b7b284c 100644 --- a/gradle-plugins/settings.gradle +++ b/gradle-plugins/settings.gradle @@ -3,4 +3,5 @@ include 'publish-utils' include 'quasar-utils' include 'cordformation' include 'cordform-common' -project(':cordform-common').projectDir = new File("$settingsDir/../cordform-common") +// TODO: Look into `includeFlat` +project(':cordform-common').projectDir = new File("$settingsDir/../cordform-common") \ No newline at end of file diff --git a/node-api/build.gradle b/node-api/build.gradle index 4e046c12d7..08ef79ae79 100644 --- a/node-api/build.gradle +++ b/node-api/build.gradle @@ -31,3 +31,11 @@ dependencies { testCompile "org.assertj:assertj-core:${assertj_version}" testCompile project(':test-utils') } + +jar { + baseName 'corda-node-api' +} + +publish { + name = jar.baseName +} 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 bda75a2bd4..e5f279c692 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/RPCStructures.kt @@ -5,9 +5,11 @@ package net.corda.nodeapi import com.esotericsoftware.kryo.Registration import com.esotericsoftware.kryo.Serializer import com.google.common.util.concurrent.ListenableFuture +import net.corda.core.requireExternal import net.corda.core.serialization.* import net.corda.core.toFuture import net.corda.core.toObservable +import net.corda.core.utilities.CordaRuntimeException import net.corda.nodeapi.config.OldConfig import rx.Observable import java.io.InputStream @@ -34,8 +36,7 @@ annotation class RPCSinceVersion(val version: Int) * Thrown to indicate a fatal error in the RPC system itself, as opposed to an error generated by the invoked * method. */ -@CordaSerializable -open class RPCException(msg: String, cause: Throwable?) : RuntimeException(msg, cause) { +open class RPCException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) { constructor(msg: String) : this(msg, null) } @@ -69,6 +70,7 @@ class RPCKryo(observableSerializer: Serializer>) : CordaKryo(mak if (ListenableFuture::class.java != type && ListenableFuture::class.java.isAssignableFrom(type)) { return super.getRegistration(ListenableFuture::class.java) } + type.requireExternal("RPC not allowed to deserialise internal classes") return super.getRegistration(type) } } diff --git a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt index 8bc6644dcf..13a70eb517 100644 --- a/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/config/SSLConfiguration.kt @@ -10,4 +10,9 @@ interface SSLConfiguration { val sslKeystore: Path get() = certificatesDirectory / "sslkeystore.jks" val nodeKeystore: Path get() = certificatesDirectory / "nodekeystore.jks" val trustStoreFile: Path get() = certificatesDirectory / "truststore.jks" -} \ No newline at end of file +} + +interface NodeSSLConfiguration : SSLConfiguration { + val baseDirectory: Path + override val certificatesDirectory: Path get() = baseDirectory / "certificates" +} diff --git a/core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ShutdownHook.kt similarity index 95% rename from core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt rename to node-api/src/main/kotlin/net/corda/nodeapi/internal/ShutdownHook.kt index 63811d1ba0..8b0f628990 100644 --- a/core/src/main/kotlin/net/corda/core/internal/ShutdownHook.kt +++ b/node-api/src/main/kotlin/net/corda/nodeapi/internal/ShutdownHook.kt @@ -1,4 +1,4 @@ -package net.corda.core.internal +package net.corda.nodeapi.internal interface ShutdownHook { /** diff --git a/node-schemas/build.gradle b/node-schemas/build.gradle index 70faf99703..5026633777 100644 --- a/node-schemas/build.gradle +++ b/node-schemas/build.gradle @@ -9,7 +9,6 @@ dependencies { compile project(':core') testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version" testCompile "junit:junit:$junit_version" - testCompile project(':test-utils') // Requery: SQL based query & persistence for Kotlin kapt "io.requery:requery-processor:$requery_version" @@ -24,4 +23,12 @@ sourceSets { srcDir "$buildDir/generated/source/kapt/main" } } +} + +jar { + baseName 'corda-node-schemas' +} + +publish { + name = jar.baseName } \ No newline at end of file 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 05c5c3f903..29b19cb1e1 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 @@ -119,7 +119,7 @@ class VaultSchemaTest { val attachments = emptyList() val id = SecureHash.randomSHA256() val signers = listOf(DUMMY_NOTARY_KEY.public) - val timestamp: Timestamp? = null + val timeWindow: TimeWindow? = null transaction = LedgerTransaction( inputs, outputs, @@ -128,7 +128,7 @@ class VaultSchemaTest { id, notary, signers, - timestamp, + timeWindow, TransactionType.General ) } @@ -151,7 +151,7 @@ class VaultSchemaTest { val attachments = emptyList() val id = SecureHash.randomSHA256() val signers = listOf(DUMMY_NOTARY_KEY.public) - val timestamp: Timestamp? = null + val timeWindow: TimeWindow? = null return LedgerTransaction( inputs, outputs, @@ -160,7 +160,7 @@ class VaultSchemaTest { id, notary, signers, - timestamp, + timeWindow, TransactionType.General ) } diff --git a/node/build.gradle b/node/build.gradle index fc8632f7ae..bf98c6139c 100644 --- a/node/build.gradle +++ b/node/build.gradle @@ -25,6 +25,9 @@ configurations { integrationTestCompile.extendsFrom testCompile integrationTestRuntime.extendsFrom testRuntime + + smokeTestCompile.extendsFrom compile + smokeTestRuntime.extendsFrom runtime } sourceSets { @@ -38,6 +41,15 @@ sourceSets { srcDir file('src/integration-test/resources') } } + 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') + } + } } // Use manual resource copying of log4j2.xml rather than source sets. @@ -46,6 +58,17 @@ processResources { from file("$rootDir/config/dev/log4j2.xml") } +processSmokeTestResources { + // Build one of the demos so that we can test CorDapp scanning in CordappScanningTest. It doesn't matter which demo + // we use, just make sure the test is updated accordingly. + from(project(':samples:trader-demo').tasks.jar) { + rename 'trader-demo-(.*)', 'trader-demo.jar' + } + from(project(':node:capsule').tasks.buildCordaJAR) { + rename 'corda-(.*)', 'corda.jar' + } +} + // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // build/reports/project/dependencies/index.html for green highlighted parts of the tree. @@ -75,7 +98,10 @@ dependencies { // Artemis: for reliable p2p message queues. compile "org.apache.activemq:artemis-server:${artemis_version}" compile "org.apache.activemq:artemis-core-client:${artemis_version}" - runtime "org.apache.activemq:artemis-amqp-protocol:${artemis_version}" + runtime ("org.apache.activemq:artemis-amqp-protocol:${artemis_version}") { + // Gains our proton-j version from core module. + exclude group: 'org.apache.qpid', module: 'proton-j' + } // JAnsi: for drawing things to the terminal in nicely coloured ways. compile "org.fusesource.jansi:jansi:$jansi_version" @@ -153,16 +179,35 @@ dependencies { compile "io.requery:requery-kotlin:$requery_version" // FastClasspathScanner: classpath scanning - compile 'io.github.lukehutch:fast-classpath-scanner:2.0.20' + compile 'io.github.lukehutch:fast-classpath-scanner:2.0.21' // Jsh: A SSH implementation for tunneling inbound traffic via a relay compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' // Integration test helpers integrationTestCompile "junit:junit:$junit_version" + integrationTestCompile "org.assertj:assertj-core:${assertj_version}" + + // Smoke tests do NOT have any Node code on the classpath! + smokeTestCompile project(':smoke-test-utils') + 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 +} + +jar { + baseName 'corda-node' +} + +publish { + name = jar.baseName +} \ No newline at end of file diff --git a/node/capsule/build.gradle b/node/capsule/build.gradle index 737251f4d9..9d60efab6c 100644 --- a/node/capsule/build.gradle +++ b/node/capsule/build.gradle @@ -23,7 +23,7 @@ task buildCordaJAR(type: FatCapsule, dependsOn: project(':node').compileJava) { applicationClass 'net.corda.node.Corda' archiveName "corda-${corda_release_version}.jar" applicationSource = files( - project(':node').configurations.compile, + project(':node').configurations.runtime, project(':node').jar, '../build/classes/main/CordaCaplet.class', '../build/classes/main/CordaCaplet$1.class', 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 30f1cf38a5..86ed4bd3af 100644 --- a/node/src/integration-test/kotlin/net/corda/node/BootTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/BootTests.kt @@ -7,7 +7,8 @@ import net.corda.core.flows.StartableByRPC import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.utilities.ALICE -import net.corda.node.driver.driver +import net.corda.testing.driver.driver +import net.corda.node.internal.NodeStartup import net.corda.node.services.startFlowPermission import net.corda.nodeapi.User import org.assertj.core.api.Assertions.assertThat @@ -43,7 +44,7 @@ class BootTests { startNode(ALICE.name).getOrThrow() } // We count the number of nodes that wrote into the logfile by counting "Logs can be found in" - val numberOfNodesThatLogged = Files.lines(logFile.toPath()).filter { it.contains(LOGS_CAN_BE_FOUND_IN_STRING) }.count() + val numberOfNodesThatLogged = Files.lines(logFile.toPath()).filter { NodeStartup.LOGS_CAN_BE_FOUND_IN_STRING in it }.count() assertEquals(1, numberOfNodesThatLogged) } } diff --git a/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt new file mode 100644 index 0000000000..1e933ddaf5 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/CordappScanningDriverTest.kt @@ -0,0 +1,54 @@ +package net.corda.node + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.util.concurrent.Futures +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.InitiatingFlow +import net.corda.core.flows.StartableByRPC +import net.corda.core.getOrThrow +import net.corda.core.identity.Party +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.ALICE +import net.corda.core.utilities.BOB +import net.corda.core.utilities.unwrap +import net.corda.node.services.startFlowPermission +import net.corda.nodeapi.User +import net.corda.testing.driver.driver +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test + +class CordappScanningDriverTest { + @Test + fun `sub-classed initiated flow pointing to the same initiating flow as its super-class`() { + val user = User("u", "p", setOf(startFlowPermission())) + // The driver will automatically pick up the annotated flows below + driver { + val (alice, bob) = Futures.allAsList( + startNode(ALICE.name, rpcUsers = listOf(user)), + startNode(BOB.name)).getOrThrow() + val initiatedFlowClass = alice.rpcClientToNode() + .start(user.username, user.password) + .proxy + .startFlow(::ReceiveFlow, bob.nodeInfo.legalIdentity) + .returnValue + assertThat(initiatedFlowClass.getOrThrow()).isEqualTo(SendSubClassFlow::class.java.name) + } + } + + @StartableByRPC + @InitiatingFlow + class ReceiveFlow(val otherParty: Party) :FlowLogic() { + @Suspendable + override fun call(): String = receive(otherParty).unwrap { it } + } + + @InitiatedBy(ReceiveFlow::class) + open class SendClassFlow(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call() = send(otherParty, javaClass.name) + } + + @InitiatedBy(ReceiveFlow::class) + class SendSubClassFlow(otherParty: Party) : SendClassFlow(otherParty) +} diff --git a/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt new file mode 100644 index 0000000000..3b42fbeae0 --- /dev/null +++ b/node/src/integration-test/kotlin/net/corda/node/NodePerformanceTests.kt @@ -0,0 +1,125 @@ +package net.corda.node + +import co.paralleluniverse.fibers.Suspendable +import com.google.common.base.Stopwatch +import com.google.common.util.concurrent.Futures +import net.corda.core.contracts.DOLLARS +import net.corda.core.flows.FlowLogic +import net.corda.core.flows.StartableByRPC +import net.corda.core.messaging.startFlow +import net.corda.core.minutes +import net.corda.core.node.services.ServiceInfo +import net.corda.core.serialization.OpaqueBytes +import net.corda.core.utilities.div +import net.corda.flows.CashIssueFlow +import net.corda.flows.CashPaymentFlow +import net.corda.node.services.startFlowPermission +import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.nodeapi.User +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.driver +import net.corda.testing.performance.startPublishingFixedRateInjector +import net.corda.testing.performance.startReporter +import net.corda.testing.performance.startTightLoopInjector +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.lang.management.ManagementFactory +import java.util.* +import java.util.concurrent.TimeUnit +import kotlin.streams.toList + + +private fun checkQuasarAgent() { + if (!(ManagementFactory.getRuntimeMXBean().inputArguments.any { it.contains("quasar") })) { + throw IllegalStateException("No quasar agent") + } +} + +@Ignore("Run these locally") +class NodePerformanceTests { + @StartableByRPC + class EmptyFlow : FlowLogic() { + @Suspendable + override fun call() { + } + } + + private data class FlowMeasurementResult( + val flowPerSecond: Double, + val averageMs: Double + ) + + @Before + fun before() { + checkQuasarAgent() + } + + @Test + fun `empty flow per second`() { + driver(startNodesInProcess = true) { + val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlowPermission())))).get() + + a.rpcClientToNode().use("A", "A") { connection -> + val timings = Collections.synchronizedList(ArrayList()) + val N = 10000 + val overallTiming = Stopwatch.createStarted().apply { + startTightLoopInjector( + parallelism = 8, + numberOfInjections = N, + queueBound = 50 + ) { + val timing = Stopwatch.createStarted().apply { + connection.proxy.startFlow(::EmptyFlow).returnValue.get() + }.stop().elapsed(TimeUnit.MICROSECONDS) + timings.add(timing) + } + }.stop().elapsed(TimeUnit.MICROSECONDS) + println( + FlowMeasurementResult( + flowPerSecond = N / (overallTiming * 0.000001), + averageMs = timings.average() * 0.001 + ) + ) + } + } + } + + @Test + fun `empty flow rate`() { + driver(startNodesInProcess = true) { + val a = startNode(rpcUsers = listOf(User("A", "A", setOf(startFlowPermission())))).get() + a as NodeHandle.InProcess + val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) + a.rpcClientToNode().use("A", "A") { connection -> + startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 2000L / TimeUnit.SECONDS) { + connection.proxy.startFlow(::EmptyFlow).returnValue.get() + } + } + } + } + + @Test + fun `self pay rate`() { + driver(startNodesInProcess = true) { + val a = startNode( + rpcUsers = listOf(User("A", "A", setOf(startFlowPermission(), startFlowPermission()))), + advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type)) + ).get() + a as NodeHandle.InProcess + val metricRegistry = startReporter(shutdownManager, a.node.services.monitoringService.metrics) + a.rpcClientToNode().use("A", "A") { connection -> + println("ISSUING") + val doneFutures = (1..100).toList().parallelStream().map { + connection.proxy.startFlow(::CashIssueFlow, 1.DOLLARS, OpaqueBytes.of(0), a.nodeInfo.legalIdentity, a.nodeInfo.notaryIdentity).returnValue + }.toList() + Futures.allAsList(doneFutures).get() + println("STARTING PAYMENT") + startPublishingFixedRateInjector(metricRegistry, 8, 5.minutes, 100L / TimeUnit.SECONDS) { + connection.proxy.startFlow(::CashPaymentFlow, 1.DOLLARS, a.nodeInfo.legalIdentity).returnValue.get() + } + } + + } + } +} \ No newline at end of file diff --git a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt index 0c4d48600b..a6aef9b4fd 100644 --- a/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/NodeStartupPerformanceTests.kt @@ -1,8 +1,8 @@ package net.corda.node import com.google.common.base.Stopwatch -import net.corda.node.driver.NetworkMapStartStrategy -import net.corda.node.driver.driver +import net.corda.testing.driver.NetworkMapStartStrategy +import net.corda.testing.driver.driver import org.junit.Ignore import org.junit.Test import java.util.* 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 155fe9c43c..0aefe4de76 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 @@ -1,82 +1,120 @@ package net.corda.node.services -import net.corda.core.contracts.DummyContract -import net.corda.core.contracts.StateAndRef -import net.corda.core.contracts.StateRef -import net.corda.core.contracts.TransactionType +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.ListenableFuture +import net.corda.core.* +import net.corda.core.contracts.* +import net.corda.core.crypto.CompositeKey +import net.corda.core.crypto.SecureHash import net.corda.core.crypto.appendToCommonName -import net.corda.core.getOrThrow import net.corda.core.identity.Party import net.corda.core.node.services.ServiceInfo -import net.corda.core.node.services.ServiceType import net.corda.core.utilities.ALICE import net.corda.core.utilities.DUMMY_NOTARY import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.flows.NotaryFlow import net.corda.node.internal.AbstractNode -import net.corda.node.internal.Node import net.corda.node.services.transactions.BFTNonValidatingNotaryService +import net.corda.node.services.transactions.minClusterSize import net.corda.node.services.transactions.minCorrectReplicas import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.node.utilities.transaction import net.corda.testing.node.NodeBasedTest import org.bouncycastle.asn1.x500.X500Name import org.junit.Test -import java.util.* -import kotlin.test.assertEquals -import kotlin.test.assertFailsWith +import java.nio.file.Files +import kotlin.test.* class BFTNotaryServiceTests : NodeBasedTest() { - @Test - fun `detect double spend`() { - val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") - startBFTNotaryCluster(clusterName, 4, BFTNonValidatingNotaryService.type) - val alice = startNode(ALICE.name).getOrThrow() - val notaryParty = alice.netMapCache.getNotary(clusterName)!! - val inputState = issueState(alice, notaryParty) - val firstTxBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState) - val firstSpendTx = alice.services.signInitialTransaction(firstTxBuilder) - alice.services.startFlow(NotaryFlow.Client(firstSpendTx)).resultFuture.getOrThrow() - val secondSpendBuilder = TransactionType.General.Builder(notaryParty).withItems(inputState).also { - it.addOutputState(DummyContract.SingleOwnerState(0, alice.info.legalIdentity)) - } - val secondSpendTx = alice.services.signInitialTransaction(secondSpendBuilder) - val secondSpend = alice.services.startFlow(NotaryFlow.Client(secondSpendTx)) - val ex = assertFailsWith(NotaryException::class) { - secondSpend.resultFuture.getOrThrow() - } - val error = ex.error as NotaryError.Conflict - assertEquals(error.txId, secondSpendTx.id) + companion object { + private val clusterName = X500Name("CN=BFT,O=R3,OU=corda,L=Zurich,C=CH") + private val serviceType = BFTNonValidatingNotaryService.type } - private fun issueState(node: AbstractNode, notary: Party) = node.run { - database.transaction { - val builder = DummyContract.generateInitial(Random().nextInt(), notary, info.legalIdentity.ref(0)) - val stx = services.signInitialTransaction(builder) - services.recordTransactions(listOf(stx)) - StateAndRef(builder.outputStates().first(), StateRef(stx.id, 0)) - } - } - - private fun startBFTNotaryCluster(clusterName: X500Name, - clusterSize: Int, - serviceType: ServiceType) { - require(clusterSize > 0) - val replicaNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } - ServiceIdentityGenerator.generateToDisk( + private fun bftNotaryCluster(clusterSize: Int): ListenableFuture { + Files.deleteIfExists("config" / "currentView") // XXX: Make config object warn if this exists? + val replicaIds = (0 until clusterSize) + val replicaNames = replicaIds.map { DUMMY_NOTARY.name.appendToCommonName(" $it") } + val party = ServiceIdentityGenerator.generateToDisk( replicaNames.map { baseDirectory(it) }, serviceType.id, - clusterName, - minCorrectReplicas(clusterSize)) - val serviceInfo = ServiceInfo(serviceType, clusterName) - val notaryClusterAddresses = (0 until clusterSize).map { "localhost:${11000 + it * 10}" } - (0 until clusterSize).forEach { + clusterName) + val advertisedServices = setOf(ServiceInfo(serviceType, clusterName)) + val config = mapOf("notaryClusterAddresses" to replicaIds.map { "localhost:${11000 + it * 10}" }) + return Futures.allAsList(replicaIds.map { startNode( replicaNames[it], - advertisedServices = setOf(serviceInfo), - configOverrides = mapOf("bftReplicaId" to it, "notaryClusterAddresses" to notaryClusterAddresses) - ).getOrThrow() + advertisedServices = advertisedServices, + configOverrides = mapOf("bftReplicaId" to it) + config + ) + }).map { party } + } + + @Test + fun `detect double spend 1 faulty`() { + detectDoubleSpend(1) + } + + @Test + fun `detect double spend 2 faulty`() { + detectDoubleSpend(2) + } + + private fun detectDoubleSpend(faultyReplicas: Int) { + val clusterSize = minClusterSize(faultyReplicas) + val aliceFuture = startNode(ALICE.name) + val notary = bftNotaryCluster(clusterSize).getOrThrow() + aliceFuture.getOrThrow().run { + val issueTx = signInitialTransaction(notary) { + addOutputState(DummyContract.SingleOwnerState(owner = info.legalIdentity)) + } + database.transaction { + services.recordTransactions(issueTx) + } + val spendTxs = (1..10).map { + signInitialTransaction(notary, true) { + addInputState(issueTx.tx.outRef(0)) + } + } + assertEquals(spendTxs.size, spendTxs.map { it.id }.distinct().size) + val flows = spendTxs.map { NotaryFlow.Client(it) } + val stateMachines = flows.map { services.startFlow(it) } + val results = stateMachines.map { ErrorOr.catch { it.resultFuture.getOrThrow() } } + val successfulIndex = results.mapIndexedNotNull { index, result -> + if (result.error == null) { + val signers = result.getOrThrow().map { it.by } + assertEquals(minCorrectReplicas(clusterSize), signers.size) + signers.forEach { + assertTrue(it in (notary.owningKey as CompositeKey).leafKeys) + } + index + } else { + null + } + }.single() + spendTxs.zip(results).forEach { (tx, result) -> + if (result.error != null) { + val error = (result.error as NotaryException).error as NotaryError.Conflict + assertEquals(tx.id, error.txId) + val (stateRef, consumingTx) = error.conflict.verified().stateHistory.entries.single() + assertEquals(StateRef(issueTx.id, 0), stateRef) + assertEquals(spendTxs[successfulIndex].id, consumingTx.id) + assertEquals(0, consumingTx.inputIndex) + assertEquals(info.legalIdentity, consumingTx.requestingParty) + } + } } } } + +private fun AbstractNode.signInitialTransaction( + notary: Party, + makeUnique: Boolean = false, + block: TransactionType.General.Builder.() -> Any? +) = services.signInitialTransaction(TransactionType.General.Builder(notary).apply { + block() + if (makeUnique) { + addAttachment(SecureHash.randomSHA256()) + } +}) diff --git a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt index 0766ba08e0..f5ec2cbeae 100644 --- a/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt +++ b/node/src/integration-test/kotlin/net/corda/node/services/DistributedServiceTests.kt @@ -14,8 +14,8 @@ 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.NodeHandle -import net.corda.node.driver.driver +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.driver import net.corda.node.services.transactions.RaftValidatingNotaryService import net.corda.nodeapi.User import net.corda.testing.expect @@ -29,7 +29,7 @@ import kotlin.test.assertEquals class DistributedServiceTests : DriverBasedTest() { lateinit var alice: NodeHandle - lateinit var notaries: List + lateinit var notaries: List lateinit var aliceProxy: CordaRPCOps lateinit var raftNotaryIdentity: Party lateinit var notaryStateMachines: Observable> @@ -52,7 +52,7 @@ class DistributedServiceTests : DriverBasedTest() { alice = aliceFuture.get() val (notaryIdentity, notaryNodes) = notariesFuture.get() raftNotaryIdentity = notaryIdentity - notaries = notaryNodes + notaries = notaryNodes.map { it as NodeHandle.OutOfProcess } assertEquals(notaries.size, clusterSize) assertEquals(notaries.size, notaries.map { it.nodeInfo.legalIdentity }.toSet().size) 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 1cba3b0b44..0669214cbf 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 @@ -22,7 +22,7 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class RaftNotaryServiceTests : NodeBasedTest() { - private val notaryName = X500Name("CN=RAFT Notary Service,O=R3,OU=corda,L=London,C=UK") + private val notaryName = X500Name("CN=RAFT Notary Service,O=R3,OU=corda,L=London,C=GB") @Test fun `detect double spend`() { @@ -62,4 +62,4 @@ class RaftNotaryServiceTests : NodeBasedTest() { 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/MQSecurityAsNodeTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt index f32f267a82..d4bb517abf 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityAsNodeTest.kt @@ -19,6 +19,7 @@ import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralSubtree import org.bouncycastle.asn1.x509.NameConstraints +import org.bouncycastle.cert.path.CertPath import org.junit.Test import java.nio.file.Files @@ -111,7 +112,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() { X509Utilities.CORDA_CLIENT_CA, clientKey.private, keyPass, - arrayOf(clientCACert, intermediateCA.certificate, rootCACert)) + CertPath(arrayOf(clientCACert, intermediateCA.certificate, rootCACert))) clientCAKeystore.save(nodeKeystore, keyStorePassword) val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeystore, keyStorePassword) @@ -119,7 +120,7 @@ class MQSecurityAsNodeTest : MQSecurityTest() { X509Utilities.CORDA_CLIENT_TLS, tlsKey.private, keyPass, - arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert)) + CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCA.certificate, rootCACert))) tlsKeystore.save(sslKeystore, keyStorePassword) } } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt index caf54da6da..7086d1a3db 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/MQSecurityTest.kt @@ -6,6 +6,7 @@ import net.corda.client.rpc.CordaRPCClient import net.corda.core.crypto.generateKeyPair import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.getOrThrow import net.corda.core.identity.Party @@ -216,7 +217,7 @@ abstract class MQSecurityTest : NodeBasedTest() { private fun startBobAndCommunicateWithAlice(): Party { val bob = startNode(BOB.name).getOrThrow() - bob.services.registerServiceFlow(SendFlow::class.java, ::ReceiveFlow) + bob.registerInitiatedFlow(ReceiveFlow::class.java) val bobParty = bob.info.legalIdentity // Perform a protocol exchange to force the peer queue to be created alice.services.startFlow(SendFlow(bobParty, 0)).resultFuture.getOrThrow() @@ -229,6 +230,7 @@ abstract class MQSecurityTest : NodeBasedTest() { override fun call() = send(otherParty, payload) } + @InitiatedBy(SendFlow::class) private class ReceiveFlow(val otherParty: Party) : FlowLogic() { @Suspendable override fun call() = receive(otherParty).unwrap { it } diff --git a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt index b4332d56b9..9ba5bac762 100644 --- a/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt +++ b/node/src/integration-test/kotlin/net/corda/services/messaging/P2PMessagingTest.kt @@ -53,7 +53,7 @@ class P2PMessagingTest : NodeBasedTest() { networkMapNode.respondWith("Hello") val alice = startNode(ALICE.name).getOrThrow() val serviceAddress = alice.services.networkMapCache.run { - alice.net.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) + alice.network.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) } val received = alice.receiveFrom(serviceAddress).getOrThrow(10.seconds) assertThat(received).isEqualTo("Hello") @@ -97,7 +97,7 @@ class P2PMessagingTest : NodeBasedTest() { val distributedServiceNodes = startNotaryCluster(DISTRIBUTED_SERVICE_NAME, 2).getOrThrow() val alice = startNode(ALICE.name, configOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)).getOrThrow() val serviceAddress = alice.services.networkMapCache.run { - alice.net.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) + alice.network.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) } val dummyTopic = "dummy.topic" @@ -106,7 +106,7 @@ class P2PMessagingTest : NodeBasedTest() { simulateCrashingNode(distributedServiceNodes, dummyTopic, responseMessage) // Send a single request with retry - val response = with(alice.net) { + val response = with(alice.network) { val request = TestRequest(replyTo = myAddress) val responseFuture = onNext(dummyTopic, request.sessionID) val msg = createMessage(TopicSession(dummyTopic), data = request.serialize().bytes) @@ -122,7 +122,7 @@ class P2PMessagingTest : NodeBasedTest() { val distributedServiceNodes = startNotaryCluster(DISTRIBUTED_SERVICE_NAME, 2).getOrThrow() val alice = startNode(ALICE.name, configOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)).getOrThrow() val serviceAddress = alice.services.networkMapCache.run { - alice.net.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) + alice.network.getAddressOfParty(getPartyInfo(getAnyNotary()!!)!!) } val dummyTopic = "dummy.topic" @@ -133,7 +133,7 @@ class P2PMessagingTest : NodeBasedTest() { val sessionId = random63BitValue() // Send a single request with retry - with(alice.net) { + with(alice.network) { val request = TestRequest(sessionId, myAddress) val msg = createMessage(TopicSession(dummyTopic), data = request.serialize().bytes) send(msg, serviceAddress, retryId = request.sessionID) @@ -147,7 +147,7 @@ class P2PMessagingTest : NodeBasedTest() { // Restart the node and expect a response val aliceRestarted = startNode(ALICE.name, configOverrides = mapOf("messageRedeliveryDelaySeconds" to 1)).getOrThrow() - val response = aliceRestarted.net.onNext(dummyTopic, sessionId).getOrThrow(5.seconds) + val response = aliceRestarted.network.onNext(dummyTopic, sessionId).getOrThrow(5.seconds) assertThat(requestsReceived.get()).isGreaterThanOrEqualTo(2) assertThat(response).isEqualTo(responseMessage) @@ -165,7 +165,7 @@ class P2PMessagingTest : NodeBasedTest() { distributedServiceNodes.forEach { val nodeName = it.info.legalIdentity.name var ignoreRequests = false - it.net.addMessageHandler(dummyTopic, DEFAULT_SESSION_ID) { netMessage, _ -> + it.network.addMessageHandler(dummyTopic, DEFAULT_SESSION_ID) { netMessage, _ -> requestsReceived.incrementAndGet() firstRequestReceived.countDown() // The node which receives the first request will ignore all requests @@ -178,8 +178,8 @@ class P2PMessagingTest : NodeBasedTest() { } else { println("sending response") val request = netMessage.data.deserialize() - val response = it.net.createMessage(dummyTopic, request.sessionID, responseMessage.serialize().bytes) - it.net.send(response, request.replyTo) + val response = it.network.createMessage(dummyTopic, request.sessionID, responseMessage.serialize().bytes) + it.network.send(response, request.replyTo) } } } @@ -192,7 +192,7 @@ class P2PMessagingTest : NodeBasedTest() { node.respondWith(node.info) } val serviceAddress = originatingNode.services.networkMapCache.run { - originatingNode.net.getAddressOfParty(getPartyInfo(getNotary(serviceName)!!)!!) + originatingNode.network.getAddressOfParty(getPartyInfo(getNotary(serviceName)!!)!!) } val participatingNodes = HashSet() // Try several times so that we can be fairly sure that any node not participating is not due to Artemis' selection @@ -208,16 +208,16 @@ class P2PMessagingTest : NodeBasedTest() { } private fun Node.respondWith(message: Any) { - net.addMessageHandler(javaClass.name, DEFAULT_SESSION_ID) { netMessage, _ -> + network.addMessageHandler(javaClass.name, DEFAULT_SESSION_ID) { netMessage, _ -> val request = netMessage.data.deserialize() - val response = net.createMessage(javaClass.name, request.sessionID, message.serialize().bytes) - net.send(response, request.replyTo) + val response = network.createMessage(javaClass.name, request.sessionID, message.serialize().bytes) + network.send(response, request.replyTo) } } private fun Node.receiveFrom(target: MessageRecipients): ListenableFuture { - val request = TestRequest(replyTo = net.myAddress) - return net.sendRequest(javaClass.name, request, target) + val request = TestRequest(replyTo = network.myAddress) + return network.sendRequest(javaClass.name, request, target) } @CordaSerializable 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 95fb7c9b23..a0eaea5923 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 @@ -3,13 +3,13 @@ package net.corda.services.messaging import com.google.common.util.concurrent.ListenableFuture import net.corda.core.crypto.X509Utilities import net.corda.core.getOrThrow -import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.random63BitValue import net.corda.core.seconds import net.corda.core.utilities.BOB import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_B +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.internal.NetworkMapInfo import net.corda.node.services.config.configureWithDevSSLCertificate import net.corda.node.services.messaging.sendRequest @@ -24,6 +24,7 @@ import net.corda.testing.node.SimpleNode import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.assertj.core.api.Assertions.assertThatThrownBy import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder import org.junit.Test import java.time.Instant import java.util.concurrent.TimeoutException @@ -56,19 +57,21 @@ class P2PSecurityTest : NodeBasedTest() { } } - private fun startSimpleNode(legalName: X500Name): SimpleNode { + private fun startSimpleNode(legalName: X500Name, + trustRoot: X509CertificateHolder? = null): SimpleNode { val config = TestNodeConfiguration( baseDirectory = baseDirectory(legalName), myLegalName = legalName, networkMapService = NetworkMapInfo(networkMapNode.configuration.p2pAddress, networkMapNode.info.legalIdentity.name)) config.configureWithDevSSLCertificate() // This creates the node's TLS cert with the CN as the legal name - return SimpleNode(config).apply { start() } + return SimpleNode(config, trustRoot = trustRoot).apply { start() } } private fun SimpleNode.registerWithNetworkMap(registrationName: X500Name): ListenableFuture { - val nodeInfo = NodeInfo(net.myAddress, Party(registrationName, identity.public), MOCK_VERSION_INFO.platformVersion) + val legalIdentity = getTestPartyAndCertificate(registrationName, identity.public) + val nodeInfo = NodeInfo(network.myAddress, legalIdentity, MOCK_VERSION_INFO.platformVersion) val registration = NodeRegistration(nodeInfo, System.currentTimeMillis(), AddOrRemove.ADD, Instant.MAX) - val request = RegistrationRequest(registration.toWire(keyService, identity.public), net.myAddress) - return net.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.net.myAddress) + val request = RegistrationRequest(registration.toWire(keyService, identity.public), network.myAddress) + return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapNode.network.myAddress) } } diff --git a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java index 3ca48441d8..8f16381e01 100644 --- a/node/src/main/java/net/corda/node/shell/FlowShellCommand.java +++ b/node/src/main/java/net/corda/node/shell/FlowShellCommand.java @@ -5,23 +5,22 @@ package net.corda.node.shell; import org.crsh.cli.*; import org.crsh.command.*; import org.crsh.text.*; +import org.crsh.text.ui.TableElement; import java.util.*; import static net.corda.node.shell.InteractiveShell.*; @Man( - "Allows you to list and start flows. This is the primary way in which you command the node to change the ledger.\n\n" + + "Allows you to start flows, list the ones available and to watch flows currently running on the node.\n\n" + + "Starting flow is the primary way in which you command the node to change the ledger.\n\n" + "This command is generic, so the right way to use it depends on the flow you wish to start. You can use the 'flow start'\n" + "command with either a full class name, or a substring of the class name that's unambiguous. The parameters to the \n" + "flow constructors (the right one is picked automatically) are then specified using the same syntax as for the run command." ) -@Usage("Start a (work)flow on the node. This is how you can change the ledger.") public class FlowShellCommand extends InteractiveShellCommand { - // Note that the class name is deliberately lower case, because we want the command the user types to be - // lower case. CRaSH should ideally lowercase the command names for us, but it doesn't. - @Command + @Usage("Start a (work)flow on the node. This is how you can change the ledger.") public void start( @Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, @Usage("The data to pass as input") @Argument(unquote = false) List input @@ -29,7 +28,16 @@ public class FlowShellCommand extends InteractiveShellCommand { startFlow(name, input, out); } - static void startFlow(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, @Usage("The data to pass as input") @Argument(unquote = false) List input, RenderPrintWriter out) { + // TODO Limit number of flows shown option? + @Command + @Usage("watch information about state machines running on the node with result information") + public void watch(InvocationContext context) throws Exception { + runStateMachinesView(out); + } + + static void startFlow(@Usage("The class name of the flow to run, or an unambiguous substring") @Argument String name, + @Usage("The data to pass as input") @Argument(unquote = false) List input, + RenderPrintWriter out) { if (name == null) { out.println("You must pass a name for the flow, see 'man flow'", Color.red); return; @@ -39,6 +47,7 @@ public class FlowShellCommand extends InteractiveShellCommand { } @Command + @Usage("list flows that user can start") public void list(InvocationContext context) throws Exception { for (String name : ops().registeredFlows()) { context.provide(name + System.lineSeparator()); diff --git a/node/src/main/kotlin/net/corda/node/Corda.kt b/node/src/main/kotlin/net/corda/node/Corda.kt index 7add085107..8cbc20e00e 100644 --- a/node/src/main/kotlin/net/corda/node/Corda.kt +++ b/node/src/main/kotlin/net/corda/node/Corda.kt @@ -2,315 +2,11 @@ package net.corda.node -import com.jcabi.manifests.Manifests -import com.jcraft.jsch.JSch -import com.jcraft.jsch.JSchException -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.node.internal.Node -import net.corda.node.internal.enforceSingleNodeIsRunning -import net.corda.node.services.config.FullNodeConfiguration -import net.corda.node.services.config.RelayConfiguration -import net.corda.node.services.transactions.bftSMaRtSerialFilter -import net.corda.node.shell.InteractiveShell -import net.corda.node.utilities.registration.HTTPNetworkRegistrationService -import net.corda.node.utilities.registration.NetworkRegistrationHelper -import org.fusesource.jansi.Ansi -import org.fusesource.jansi.AnsiConsole -import org.slf4j.LoggerFactory -import org.slf4j.bridge.SLF4JBridgeHandler -import java.io.IOException -import java.lang.management.ManagementFactory -import java.net.InetAddress -import java.nio.file.Paths -import java.util.* -import kotlin.system.exitProcess - -private var renderBasicInfoToConsole = true - -/** Used for useful info that we always want to show, even when not logging to the console */ -fun printBasicNodeInfo(description: String, info: String? = null) { - val msg = if (info == null) description else "${description.padEnd(40)}: $info" - val loggerName = if (renderBasicInfoToConsole) "BasicInfo" else "Main" - LoggerFactory.getLogger(loggerName).info(msg) -} - -val LOGS_DIRECTORY_NAME = "logs" -val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" -private val log by lazy { LoggerFactory.getLogger("Main") } - -private fun initLogging(cmdlineOptions: CmdLineOptions) { - val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH) - System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. - if (cmdlineOptions.logToConsole) { - System.setProperty("consoleLogLevel", loggingLevel) - renderBasicInfoToConsole = false - } - System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) - SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. - SLF4JBridgeHandler.install() -} +import net.corda.node.internal.NodeStartup fun main(args: Array) { - val startTime = System.currentTimeMillis() - assertCanNormalizeEmptyPath() - - val argsParser = ArgsParser() - - val cmdlineOptions = try { - argsParser.parse(*args) - } catch (ex: OptionException) { - println("Invalid command line arguments: ${ex.message}") - argsParser.printHelp(System.out) - exitProcess(1) - } - - // We do the single node check before we initialise logging so that in case of a double-node start it doesn't mess - // with the running node's logs. - enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory) - - initLogging(cmdlineOptions) - // Manifest properties are only available if running from the corda jar - fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null - - val versionInfo = VersionInfo( - manifestValue("Corda-Platform-Version")?.toInt() ?: 1, - manifestValue("Corda-Release-Version") ?: "Unknown", - manifestValue("Corda-Revision") ?: "Unknown", - manifestValue("Corda-Vendor") ?: "Unknown" - ) - - if (cmdlineOptions.isVersion) { - println("${versionInfo.vendor} ${versionInfo.releaseVersion}") - println("Revision ${versionInfo.revision}") - println("Platform Version ${versionInfo.platformVersion}") - exitProcess(0) - } - - // Maybe render command line help. - if (cmdlineOptions.help) { - argsParser.printHelp(System.out) - exitProcess(0) - } - - drawBanner(versionInfo) - - printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) - - val conf = try { - cmdlineOptions.loadConfig() - } catch (e: ConfigException) { - println("Unable to load the configuration file: ${e.rootCause.message}") - exitProcess(2) - } - - SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) - - conf.relay?.let { connectToRelay(it, conf.p2pAddress.port) } - - if (cmdlineOptions.isRegistration) { - println() - println("******************************************************************") - println("* *") - println("* Registering as a new participant with Corda network *") - println("* *") - println("******************************************************************") - NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(conf.certificateSigningService)).buildKeystore() - exitProcess(0) - } - - log.info("Vendor: ${versionInfo.vendor}") - log.info("Release: ${versionInfo.releaseVersion}") - log.info("Platform Version: ${versionInfo.platformVersion}") - log.info("Revision: ${versionInfo.revision}") - val info = ManagementFactory.getRuntimeMXBean() - log.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this - log.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}") - log.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}") - log.info("Application Args: ${args.joinToString(" ")}") - log.info("bootclasspath: ${info.bootClassPath}") - log.info("classpath: ${info.classPath}") - log.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}") - log.info("Machine: ${lookupMachineNameAndMaybeWarn()}") - log.info("Working Directory: ${cmdlineOptions.baseDirectory}") - val agentProperties = sun.misc.VMSupport.getAgentProperties() - if (agentProperties.containsKey("sun.jdwp.listenerAddress")) { - log.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}") - } - log.info("Starting as node on ${conf.p2pAddress}") - - try { - cmdlineOptions.baseDirectory.createDirectories() - - val node = conf.createNode(versionInfo) - node.start() - printPluginsAndServices(node) - - node.networkMapRegistrationFuture.success { - val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 - // 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 - node.startupComplete then { - try { - InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) - } catch(e: Throwable) { - log.error("Shell failed to start", e) - } - } - } failure { - log.error("Error during network map registration", it) - exitProcess(1) - } - node.run() - } catch (e: Exception) { - log.error("Exception during node startup", e) - exitProcess(1) - } - - exitProcess(0) -} - -private fun connectToRelay(config: RelayConfiguration, localBrokerPort: Int) { - with(config) { - val jsh = JSch().apply { - val noPassphrase = byteArrayOf() - addIdentity(privateKeyFile.toString(), publicKeyFile.toString(), noPassphrase) - } - - val session = jsh.getSession(username, relayHost, sshPort).apply { - // We don't check the host fingerprints because they may change often - setConfig("StrictHostKeyChecking", "no") - } - - try { - log.info("Connecting to a relay at $relayHost") - session.connect() - } catch (e: JSchException) { - throw IOException("Unable to establish a SSH connection: $username@$relayHost", e) - } - try { - val localhost = "127.0.0.1" - log.info("Forwarding ports: $relayHost:$remoteInboundPort -> $localhost:$localBrokerPort") - session.setPortForwardingR(remoteInboundPort, localhost, localBrokerPort) - } catch (e: JSchException) { - throw IOException("Unable to set up port forwarding - is SSH on the remote host configured correctly? " + - "(port forwarding is not enabled by default)", e) - } - } - - log.info("Relay setup successfully!") -} - -private fun lookupMachineNameAndMaybeWarn(): String { - val start = System.currentTimeMillis() - val hostName: String = InetAddress.getLocalHost().hostName - val elapsed = System.currentTimeMillis() - start - if (elapsed > 1000 && hostName.endsWith(".local")) { - // User is probably on macOS and experiencing this problem: http://stackoverflow.com/questions/10064581/how-can-i-eliminate-slow-resolving-loading-of-localhost-virtualhost-a-2-3-secon - // - // Also see https://bugs.openjdk.java.net/browse/JDK-8143378 - val messages = listOf( - "Your computer took over a second to resolve localhost due an incorrect configuration. Corda will work but start very slowly until this is fixed. ", - "Please see https://docs.corda.net/getting-set-up-fault-finding.html#slow-localhost-resolution for information on how to fix this. ", - "It will only take a few seconds for you to resolve." - ) - log.warn(messages.joinToString("")) - Emoji.renderIfSupported { - print(Ansi.ansi().fgBrightRed()) - messages.forEach { - println("${Emoji.sleepingFace}$it") - } - print(Ansi.ansi().reset()) - } - } - return hostName -} - -private fun assertCanNormalizeEmptyPath() { - // Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83 - try { - Paths.get("").normalize() - } catch (e: ArrayIndexOutOfBoundsException) { - failStartUp("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.") - } -} - -internal fun failStartUp(message: String): Nothing { - println(message) - println("Corda will now exit...") - exitProcess(1) -} - -private fun printPluginsAndServices(node: Node) { - node.configuration.extraAdvertisedServiceIds.let { - if (it.isNotEmpty()) printBasicNodeInfo("Providing network services", it.joinToString()) - } - val plugins = node.pluginRegistries - .map { it.javaClass.name } - .filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") } - .map { it.substringBefore('$') } - if (plugins.isNotEmpty()) - printBasicNodeInfo("Loaded plugins", plugins.joinToString()) -} - -private fun messageOfTheDay(): Pair { - val messages = arrayListOf( - "The only distributed ledger that pays\nhomage to Pac Man in its logo.", - "You know, I was a banker once ...\nbut I lost interest. ${Emoji.bagOfCash}", - "It's not who you know, it's who you know\nknows what you know you know.", - "It runs on the JVM because QuickBasic\nis apparently not 'professional' enough.", - "\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"", - "It's kind of like a block chain but\ncords sounded healthier than chains.", - "Computer science and finance together.\nYou should see our crazy Christmas parties!", - "I met my bank manager yesterday and asked\nto check my balance ... he pushed me over!", - "A banker with nobody around may find\nthemselves .... a-loan! ", - "Whenever I go near my bank I get\nwithdrawal symptoms ${Emoji.coolGuy}", - "There was an earthquake in California,\na local bank went into de-fault.", - "I asked for insurance if the nearby\nvolcano erupted. They said I'd be covered.", - "I had an account with a bank in the\nNorth Pole, but they froze all my assets ${Emoji.santaClaus}", - "Check your contracts carefully. The\nfine print is usually a clause for suspicion ${Emoji.santaClaus}", - "Some bankers are generous ...\nto a vault! ${Emoji.bagOfCash} ${Emoji.coolGuy}", - "What you can buy for a dollar these\ndays is absolute non-cents! ${Emoji.bagOfCash}", - "Old bankers never die, they just\n... pass the buck", - "My wife made me into millionaire.\nI was a multi-millionaire before we met.", - "I won $3M on the lottery so I donated\na quarter of it to charity. Now I have $2,999,999.75.", - "There are two rules for financial success:\n1) Don't tell everything you know.", - "Top tip: never say \"oops\", instead\nalways say \"Ah, Interesting!\"", - "Computers are useless. They can only\ngive you answers. -- Picasso" - ) - if (Emoji.hasEmojiTerminal) - messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}" - val (a, b) = messages.randomOrNull()!!.split('\n') - return Pair(a, b) -} - -private fun drawBanner(versionInfo: VersionInfo) { - // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. - AnsiConsole.systemInstall() - - Emoji.renderIfSupported { - val (msg1, msg2) = messageOfTheDay() - - println(Ansi.ansi().fgBrightRed().a(""" - ______ __ - / ____/ _________/ /___ _ - / / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a( -"/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a( -"""\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold(). - a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------"). - newline(). - newline(). - a("${Emoji.books}New! ").reset().a("Training now available worldwide, see https://corda.net/corda-training/"). - newline(). - reset()) - } + // Pass the arguments to the Node factory. In the Enterprise edition, this line is modified to point to a subclass. + // It will exit the process in case of startup failure and is not intended to be used by embedders. If you want + // to embed Node in your own container, instantiate it directly and set up the configuration objects yourself. + NodeStartup(args).run() } diff --git a/node/src/main/kotlin/net/corda/node/SerialFilter.kt b/node/src/main/kotlin/net/corda/node/SerialFilter.kt index 8ff753f42b..ef6ebe138d 100644 --- a/node/src/main/kotlin/net/corda/node/SerialFilter.kt +++ b/node/src/main/kotlin/net/corda/node/SerialFilter.kt @@ -1,6 +1,8 @@ package net.corda.node -import java.lang.reflect.Field +import net.corda.core.DeclaredField +import net.corda.core.DeclaredField.Companion.declaredField +import net.corda.node.internal.Node import java.lang.reflect.Method import java.lang.reflect.Proxy @@ -10,7 +12,7 @@ internal object SerialFilter { private val undecided: Any private val rejected: Any private val serialFilterLock: Any - private val serialFilterField: Field + private val serialFilterField: DeclaredField init { // ObjectInputFilter and friends are in java.io in Java 9 but sun.misc in backports: @@ -24,14 +26,14 @@ internal object SerialFilter { // JDK 8u121 is the earliest JDK8 JVM that supports this functionality. filterInterface = getFilterInterface("java.io") ?: getFilterInterface("sun.misc") - ?: failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121.") + ?: Node.failStartUp("Corda forbids Java deserialisation. Please upgrade to at least JDK 8u121.") serialClassGetter = Class.forName("${filterInterface.name}\$FilterInfo").getMethod("serialClass") val statusEnum = Class.forName("${filterInterface.name}\$Status") undecided = statusEnum.getField("UNDECIDED").get(null) rejected = statusEnum.getField("REJECTED").get(null) val configClass = Class.forName("${filterInterface.name}\$Config") - serialFilterLock = configClass.getDeclaredField("serialFilterLock").also { it.isAccessible = true }.get(null) - serialFilterField = configClass.getDeclaredField("serialFilter").also { it.isAccessible = true } + serialFilterLock = declaredField(configClass, "serialFilterLock").value + serialFilterField = declaredField(configClass, "serialFilter") } internal fun install(acceptClass: (Class<*>) -> Boolean) { @@ -45,7 +47,7 @@ internal object SerialFilter { } // Can't simply use the setter as in non-trampoline mode Capsule has inited the filter in premain: synchronized(serialFilterLock) { - serialFilterField.set(null, filter) + serialFilterField.value = filter } } 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 348699c273..f534ae4a38 100644 --- a/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt +++ b/node/src/main/kotlin/net/corda/node/internal/AbstractNode.kt @@ -2,27 +2,30 @@ package net.corda.node.internal import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting +import com.google.common.collect.MutableClassToInstanceMap 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 io.github.lukehutch.fastclasspathscanner.scanner.ScanResult import net.corda.core.* 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.flows.* import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.RPCOps 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.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.debug +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.* import net.corda.node.services.* import net.corda.node.services.api.* @@ -45,7 +48,7 @@ import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.statemachine.FlowStateMachineImpl import net.corda.node.services.statemachine.StateMachineManager -import net.corda.node.services.statemachine.flowVersion +import net.corda.node.services.statemachine.flowVersionAndInitiatingClass import net.corda.node.services.transactions.* import net.corda.node.services.vault.CashBalanceAsMetricsObserver import net.corda.node.services.vault.NodeVaultService @@ -56,23 +59,26 @@ 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 rx.Observable import java.io.IOException import java.lang.reflect.Modifier.* -import java.net.InetAddress -import java.net.URL +import java.net.JarURLConnection +import java.net.URI import java.nio.file.FileAlreadyExistsException import java.nio.file.Path import java.nio.file.Paths import java.security.KeyPair +import java.security.KeyStore import java.security.KeyStoreException +import java.security.cert.* import java.time.Clock import java.util.* import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ExecutorService import java.util.concurrent.TimeUnit.SECONDS +import java.util.stream.Collectors.toList import kotlin.collections.ArrayList import kotlin.reflect.KClass import net.corda.core.crypto.generateKeyPair as cryptoGenerateKeyPair @@ -106,11 +112,12 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // low-performance prototyping period. protected abstract val serverThread: AffinityExecutor - protected val serviceFlowFactories = ConcurrentHashMap, ServiceFlowInfo>() + private val cordappServices = MutableClassToInstanceMap.create() + private val flowFactories = ConcurrentHashMap>, InitiatedFlowFactory<*>>() protected val partyKeys = mutableSetOf() val services = object : ServiceHubInternal() { - override val networkService: MessagingService get() = net + override val networkService: MessagingService get() = network override val networkMapCache: NetworkMapCacheInternal get() = netMapCache override val storageService: TxWritableStorageService get() = storage override val vaultService: VaultService get() = vault @@ -122,6 +129,12 @@ 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 fun cordaService(type: Class): T { + require(type.isAnnotationPresent(CordaService::class.java)) { "${type.name} is not a Corda service" } + return cordappServices.getInstance(type) ?: throw IllegalArgumentException("Corda service ${type.name} does not exist") + } + override val rpcFlows: List>> get() = this@AbstractNode.rpcFlows // Internal only @@ -131,17 +144,8 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return serverThread.fetchFrom { smm.add(logic, flowInitiator) } } - override fun registerServiceFlow(initiatingFlowClass: Class>, serviceFlowFactory: (Party) -> FlowLogic<*>) { - require(initiatingFlowClass !in serviceFlowFactories) { - "${initiatingFlowClass.name} has already been used to register a service flow" - } - val info = ServiceFlowInfo.CorDapp(initiatingFlowClass.flowVersion, serviceFlowFactory) - log.info("Registering service flow for ${initiatingFlowClass.name}: $info") - serviceFlowFactories[initiatingFlowClass] = info - } - - override fun getServiceFlowFactory(clientFlowClass: Class>): ServiceFlowInfo? { - return serviceFlowFactories[clientFlowClass] + override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? { + return flowFactories[initiatingFlowClass] } override fun recordTransactions(txs: Iterable) { @@ -151,7 +155,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - open fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity] + open fun findMyLocation(): PhysicalLocation? { + return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } + } lateinit var info: NodeInfo lateinit var storage: TxWritableStorageService @@ -162,20 +168,16 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, var inNodeNetworkMapService: NetworkMapService? = null lateinit var txVerifierService: TransactionVerifierService lateinit var identity: IdentityService - lateinit var net: MessagingService + lateinit var network: MessagingService lateinit var netMapCache: NetworkMapCacheInternal lateinit var scheduler: NodeSchedulerService lateinit var schemas: SchemaService lateinit var auditService: AuditService - val customServices: ArrayList = ArrayList() - protected val runOnStop: ArrayList = ArrayList() + protected val runOnStop = ArrayList<() -> Any?>() lateinit var database: Database - protected var dbCloser: Runnable? = null + protected var dbCloser: (() -> Any?)? = 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() - var isPreviousCheckpointsPresent = false private set @@ -217,13 +219,15 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, val tokenizableServices = makeServices() smm = StateMachineManager(services, - listOf(tokenizableServices), checkpointStorage, serverThread, database, busyNodeLatch) + + smm.tokenizableServices.addAll(tokenizableServices) + if (serverThread is ExecutorService) { - runOnStop += Runnable { + runOnStop += { // We wait here, even though any in-flight messages should have been drained away because the // server thread can potentially have other non-messaging tasks scheduled onto it. The timeout value is // arbitrary and might be inappropriate. @@ -240,42 +244,186 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, startMessagingService(rpcOps) installCoreFlows() - fun Class>.isUserInvokable(): Boolean { - return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) + val scanResult = scanCordapps() + if (scanResult != null) { + installCordaServices(scanResult) + registerInitiatedFlows(scanResult) + rpcFlows = findRPCFlows(scanResult) + } else { + rpcFlows = emptyList() } - 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) + // TODO Remove this once the cash stuff is in its own CorDapp + registerInitiatedFlow(IssuerFlow.Issuer::class.java) - runOnStop += Runnable { net.stop() } + initUploaders() + + runOnStop += network::stop _networkMapRegistrationFuture.setFuture(registerWithNetworkMapIfConfigured()) smm.start() // Shut down the SMM so no Fibers are scheduled. - runOnStop += Runnable { smm.stop(acceptableLiveFiberCountOnStop()) } + runOnStop += { smm.stop(acceptableLiveFiberCountOnStop()) } scheduler.start() } started = true return this } + private fun installCordaServices(scanResult: ScanResult) { + fun getServiceType(clazz: Class<*>): ServiceType? { + return try { + clazz.getField("type").get(null) as ServiceType + } catch (e: NoSuchFieldException) { + log.warn("${clazz.name} does not have a type field, optimistically proceeding with install.") + null + } + } + + return scanResult.getClassesWithAnnotation(SerializeAsToken::class, CordaService::class) + .filter { + val serviceType = getServiceType(it) + if (serviceType != null && info.serviceIdentities(serviceType).isEmpty()) { + log.debug { "Ignoring ${it.name} as a Corda service since $serviceType is not one of our " + + "advertised services" } + false + } else { + true + } + } + .forEach { + try { + installCordaService(it) + } catch (e: NoSuchMethodException) { + log.error("${it.name}, as a Corda service, must have a constructor with a single parameter " + + "of type ${PluginServiceHub::class.java.name}") + } catch (e: Exception) { + log.error("Unable to install Corda service ${it.name}", e) + } + } + } + + /** + * Use this method to install your Corda services in your tests. This is automatically done by the node when it + * starts up for all classes it finds which are annotated with [CordaService]. + */ + fun installCordaService(clazz: Class): T { + clazz.requireAnnotation() + val ctor = clazz.getDeclaredConstructor(PluginServiceHub::class.java).apply { isAccessible = true } + val service = ctor.newInstance(services) + cordappServices.putInstance(clazz, service) + smm.tokenizableServices += service + log.info("Installed ${clazz.name} Corda service") + return service + } + + private inline fun Class<*>.requireAnnotation(): A { + return requireNotNull(getDeclaredAnnotation(A::class.java)) { "$name needs to be annotated with ${A::class.java.name}" } + } + + private fun registerInitiatedFlows(scanResult: ScanResult) { + scanResult + .getClassesWithAnnotation(FlowLogic::class, InitiatedBy::class) + // First group by the initiating flow class in case there are multiple mappings + .groupBy { it.requireAnnotation().value.java } + .map { (initiatingFlow, initiatedFlows) -> + val sorted = initiatedFlows.sortedWith(FlowTypeHierarchyComparator(initiatingFlow)) + if (sorted.size > 1) { + log.warn("${initiatingFlow.name} has been specified as the inititating flow by multiple flows " + + "in the same type hierarchy: ${sorted.joinToString { it.name }}. Choosing the most " + + "specific sub-type for registration: ${sorted[0].name}.") + } + sorted[0] + } + .forEach { + try { + registerInitiatedFlowInternal(it, track = false) + } catch (e: NoSuchMethodException) { + log.error("${it.name}, as an initiated flow, must have a constructor with a single parameter " + + "of type ${Party::class.java.name}") + } catch (e: Exception) { + log.error("Unable to register initiated flow ${it.name}", e) + } + } + } + + private class FlowTypeHierarchyComparator(val initiatingFlow: Class>) : Comparator>> { + override fun compare(o1: Class>, o2: Class>): Int { + return if (o1 == o2) { + 0 + } else if (o1.isAssignableFrom(o2)) { + 1 + } else if (o2.isAssignableFrom(o1)) { + -1 + } else { + throw IllegalArgumentException("${initiatingFlow.name} has been specified as the initiating flow by " + + "both ${o1.name} and ${o2.name}") + } + } + } + + /** + * Use this method to register your initiated flows in your tests. This is automatically done by the node when it + * starts up for all [FlowLogic] classes it finds which are annotated with [InitiatedBy]. + * @return An [Observable] of the initiated flows started by counter-parties. + */ + fun > registerInitiatedFlow(initiatedFlowClass: Class): Observable { + return registerInitiatedFlowInternal(initiatedFlowClass, track = true) + } + + private fun > registerInitiatedFlowInternal(initiatedFlow: Class, track: Boolean): Observable { + val ctor = initiatedFlow.getDeclaredConstructor(Party::class.java).apply { isAccessible = true } + val initiatingFlow = initiatedFlow.requireAnnotation().value.java + val (version, classWithAnnotation) = initiatingFlow.flowVersionAndInitiatingClass + require(classWithAnnotation == initiatingFlow) { + "${InitiatingFlow::class.java.name} must be annotated on ${initiatingFlow.name} and not on a super-type" + } + val flowFactory = InitiatedFlowFactory.CorDapp(version, { ctor.newInstance(it) }) + val observable = internalRegisterFlowFactory(initiatingFlow, flowFactory, initiatedFlow, track) + log.info("Registered ${initiatingFlow.name} to initiate ${initiatedFlow.name} (version $version)") + return observable + } + + @VisibleForTesting + fun > internalRegisterFlowFactory(initiatingFlowClass: Class>, + flowFactory: InitiatedFlowFactory, + initiatedFlowClass: Class, + track: Boolean): Observable { + val observable = if (track) { + smm.changes.filter { it is StateMachineManager.Change.Add }.map { it.logic }.ofType(initiatedFlowClass) + } else { + Observable.empty() + } + flowFactories[initiatingFlowClass] = flowFactory + return observable + } + + private fun findRPCFlows(scanResult: ScanResult): List>> { + fun Class>.isUserInvokable(): Boolean { + return isPublic(modifiers) && !isLocalClass && !isAnonymousClass && (!isMemberClass || isStatic(modifiers)) + } + + return scanResult.getClassesWithAnnotation(FlowLogic::class, StartableByRPC::class).filter { it.isUserInvokable() } + + // 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) + } + /** * Installs a flow that's core to the Corda platform. Unlike CorDapp flows which are versioned individually using * [InitiatingFlow.version], core flows have the same version as the node's platform version. To cater for backwards - * compatibility [serviceFlowFactory] provides a second parameter which is the platform version of the initiating party. + * compatibility [flowFactory] provides a second parameter which is the platform version of the initiating party. * @suppress */ @VisibleForTesting - fun installCoreFlow(clientFlowClass: KClass>, serviceFlowFactory: (Party, Int) -> FlowLogic<*>) { - require(clientFlowClass.java.flowVersion == 1) { + fun installCoreFlow(clientFlowClass: KClass>, flowFactory: (Party, Int) -> FlowLogic<*>) { + require(clientFlowClass.java.flowVersionAndInitiatingClass.first == 1) { "${InitiatingFlow::class.java.name}.version not applicable for core flows; their version is the node's platform version" } - serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.Core(serviceFlowFactory) + flowFactories[clientFlowClass.java] = InitiatedFlowFactory.Core(flowFactory) log.debug { "Installed core flow ${clientFlowClass.java.name}" } } @@ -296,7 +444,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, storage = storageServices.first checkpointStorage = storageServices.second netMapCache = InMemoryNetworkMapCache() - net = makeMessagingService() + network = makeMessagingService() schemas = makeSchemaService() vault = makeVaultService(configuration.dataSourceProperties) txVerifierService = makeTransactionVerifierService() @@ -307,67 +455,69 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Place the long term identity key in the KMS. Eventually, this is likely going to be separated again because // the KMS is meant for derived temporary keys used in transactions, and we're not supposed to sign things with // the identity key. But the infrastructure to make that easy isn't here yet. - keyManagement = makeKeyManagementService() + keyManagement = makeKeyManagementService(identity) scheduler = NodeSchedulerService(services, database, unfinishedSchedules = busyNodeLatch) - val tokenizableServices = mutableListOf(storage, net, vault, keyManagement, identity, platformClock, scheduler) + val tokenizableServices = mutableListOf(storage, network, vault, keyManagement, identity, platformClock, scheduler) makeAdvertisedServices(tokenizableServices) - - customServices.clear() - customServices.addAll(makePluginServices(tokenizableServices)) - - initUploaders(storageServices) 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() + private fun scanCordapps(): ScanResult? { + val scanPackage = System.getProperty("net.corda.node.cordapp.scan.package") + val paths = if (scanPackage != null) { + // Rather than looking in the plugins directory, figure out the classpath for the given package and scan that + // instead. This is used in tests where we avoid having to package stuff up in jars and then having to move + // them to the plugins directory for each node. + check(configuration.devMode) { "Package scanning can only occur in dev mode" } + val resource = scanPackage.replace('.', '/') + javaClass.classLoader.getResources(resource) + .asSequence() + .map { + val uri = if (it.protocol == "jar") { + (it.openConnection() as JarURLConnection).jarFileURL.toURI() + } else { + URI(it.toExternalForm().removeSuffix(resource)) + } + Paths.get(uri) + } + .toList() + } else { + val pluginsDir = configuration.baseDirectory / "plugins" + if (!pluginsDir.exists()) return null + pluginsDir.list { + it.filter { it.isRegularFile() && it.toString().endsWith(".jar") }.collect(toList()) + } } - if (pluginJars.isEmpty()) return emptyList() + log.info("Scanning CorDapps in $paths") - val scanResult = FastClasspathScanner().overrideClasspath(*pluginJars).scan() // This will only scan the plugin jars and nothing else + // This will only scan the plugin jars and nothing else + return if (paths.isNotEmpty()) FastClasspathScanner().overrideClasspath(paths).scan() else null + } - fun loadFlowClass(className: String): Class>? { + private fun ScanResult.getClassesWithAnnotation(type: KClass, annotation: KClass): List> { + fun loadClass(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> + Class.forName(className, false, javaClass.classLoader).asSubclass(type.java) + } catch (e: ClassCastException) { + log.warn("As $className is annotated with ${annotation.qualifiedName} it must be a sub-type of ${type.java.name}") + null } catch (e: Exception) { - log.warn("Unable to load flow class $className", e) + log.warn("Unable to load class $className", e) null } } - val flowClasses = scanResult.getNamesOfSubclassesOf(FlowLogic::class.java) - .mapNotNull { loadFlowClass(it) } + return getNamesOfClassesWithAnnotation(annotation.java) + .mapNotNull { loadClass(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) + private fun initUploaders() { + val uploaders: List = listOf(storage.attachments as NodeAttachmentService) + + cordappServices.values.filterIsInstance(AcceptsFileUpload::class.java) (storage as StorageServiceImpl).initUploaders(uploaders) } @@ -381,7 +531,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun makeInfo(): NodeInfo { val advertisedServiceEntries = makeServiceEntries() val legalIdentity = obtainLegalIdentity() - return NodeInfo(net.myAddress, legalIdentity, platformVersion, advertisedServiceEntries, findMyLocation()) + return NodeInfo(network.myAddress, legalIdentity, platformVersion, advertisedServiceEntries, findMyLocation()) } /** @@ -403,7 +553,9 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, private fun hasSSLCertificates(): Boolean { val (sslKeystore, keystore) = try { // This will throw IOException if key file not found or KeyStoreException if keystore password is incorrect. - Pair(KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) + Pair( + KeyStoreUtilities.loadKeyStore(configuration.sslKeystore, configuration.keyStorePassword), + KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword)) } catch (e: IOException) { return false } catch (e: KeyStoreException) { @@ -423,8 +575,10 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, this.database = database // Now log the vendor string as this will also cause a connection to be tested eagerly. log.info("Connected to ${database.vendor} database.") - dbCloser = Runnable { toClose.close() } - runOnStop += dbCloser!! + toClose::close.let { + dbCloser = it + runOnStop += it + } database.transaction { insideTransaction() } @@ -433,12 +587,6 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } - private fun makePluginServices(tokenizableServices: MutableList): List { - val pluginServices = pluginRegistries.flatMap { it.servicePlugins }.map { it.apply(services) } - tokenizableServices.addAll(pluginServices) - return pluginServices - } - /** * Run any tasks that are needed to ensure the node is in a correct state before running start(). */ @@ -481,7 +629,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, return sendNetworkMapRegistration(address).flatMap { (error) -> check(error == null) { "Unable to register with the network map service: $error" } // The future returned addMapService will complete on the same executor as sendNetworkMapRegistration, namely the one used by net - services.networkMapCache.addMapService(net, address, true, null) + services.networkMapCache.addMapService(network, address, true, null) } } @@ -491,8 +639,8 @@ 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(keyManagement, legalIdentityKey.public), net.myAddress) - return net.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) + val request = NetworkMapService.RegistrationRequest(reg.toWire(keyManagement, legalIdentityKey.public), network.myAddress) + return network.sendRequest(NetworkMapService.REGISTER_TOPIC, request, networkMapAddress) } /** This is overriden by the mock node implementation to enable operation without any network map service */ @@ -502,27 +650,31 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "has any other map node been configured.") } - protected open fun makeKeyManagementService(): KeyManagementService = PersistentKeyManagementService(partyKeys) + protected open fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { + return PersistentKeyManagementService(identityService, partyKeys) + } open protected fun makeNetworkMapService() { inNodeNetworkMapService = PersistentNetworkMapService(services, configuration.minimumPlatformVersion) } open protected fun makeNotaryService(type: ServiceType, tokenizableServices: MutableList) { - val timestampChecker = TimestampChecker(platformClock, 30.seconds) + val timeWindowChecker = TimeWindowChecker(platformClock, 30.seconds) val uniquenessProvider = makeUniquenessProvider(type) tokenizableServices.add(uniquenessProvider) val notaryService = when (type) { - SimpleNotaryService.type -> SimpleNotaryService(timestampChecker, uniquenessProvider) - ValidatingNotaryService.type -> ValidatingNotaryService(timestampChecker, uniquenessProvider) - RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timestampChecker, uniquenessProvider as RaftUniquenessProvider) - RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timestampChecker, uniquenessProvider as RaftUniquenessProvider) + SimpleNotaryService.type -> SimpleNotaryService(timeWindowChecker, uniquenessProvider) + ValidatingNotaryService.type -> ValidatingNotaryService(timeWindowChecker, uniquenessProvider) + RaftNonValidatingNotaryService.type -> RaftNonValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) + RaftValidatingNotaryService.type -> RaftValidatingNotaryService(timeWindowChecker, uniquenessProvider as RaftUniquenessProvider) BFTNonValidatingNotaryService.type -> with(configuration as FullNodeConfiguration) { val replicaId = bftReplicaId ?: throw IllegalArgumentException("bftReplicaId value must be specified in the configuration") BFTSMaRtConfig(notaryClusterAddresses).use { config -> - val client = BFTSMaRt.Client(config, replicaId).also { tokenizableServices += it } // (Ab)use replicaId for clientId. - BFTNonValidatingNotaryService(config, services, timestampChecker, replicaId, database, client) + BFTNonValidatingNotaryService(config, services, timeWindowChecker, replicaId, database).also { + tokenizableServices += it.client + runOnStop += it::dispose + } } } else -> { @@ -536,13 +688,14 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, protected abstract fun makeUniquenessProvider(type: ServiceType): UniquenessProvider protected open fun makeIdentityService(): IdentityService { - val service = InMemoryIdentityService() - service.registerIdentity(info.legalIdentity) - services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentity) } + val keyStore = KeyStoreUtilities.loadKeyStore(configuration.trustStoreFile, configuration.trustStorePassword) + val trustRoot = keyStore.getCertificate(X509Utilities.CORDA_ROOT_CA) as? X509Certificate + val service = InMemoryIdentityService(setOf(info.legalIdentityAndCert), trustRoot = trustRoot) + services.networkMapCache.partyNodes.forEach { service.registerIdentity(it.legalIdentityAndCert) } netMapCache.changed.subscribe { mapChange -> // TODO how should we handle network map removal if (mapChange is MapChange.Added) { - service.registerIdentity(mapChange.node.legalIdentity) + service.registerIdentity(mapChange.node.legalIdentityAndCert) } } return service @@ -564,7 +717,7 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // Run shutdown hooks in opposite order to starting for (toRun in runOnStop.reversed()) { - toRun.run() + toRun() } runOnStop.clear() } @@ -589,10 +742,11 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, stateMachineRecordedTransactionMappingStorage: StateMachineRecordedTransactionMappingStorage) = StorageServiceImpl(attachments, transactionStorage, stateMachineRecordedTransactionMappingStorage) - protected fun obtainLegalIdentity(): Party = obtainKeyPair().first - protected fun obtainLegalIdentityKey(): KeyPair = obtainKeyPair().second + protected fun obtainLegalIdentity(): PartyAndCertificate = identityKeyPair.first + protected fun obtainLegalIdentityKey(): KeyPair = identityKeyPair.second + private val identityKeyPair by lazy { obtainKeyPair("identity", configuration.myLegalName) } - private fun obtainKeyPair(serviceId: String = "identity", serviceName: X500Name = configuration.myLegalName): Pair { + private fun obtainKeyPair(serviceId: String, serviceName: X500Name): 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 @@ -600,21 +754,23 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, // the legal name is actually validated in some way. // TODO: Integrate with Key management service? - val keystore = KeyStoreUtilities.loadKeyStore(configuration.nodeKeystore, configuration.keyStorePassword) - val clientCA = keystore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, configuration.keyStorePassword) + val certFactory = CertificateFactory.getInstance("X509") + val keyStore = KeyStoreWrapper(configuration.nodeKeystore, configuration.keyStorePassword) val privateKeyAlias = "$serviceId-private-key" val privKeyFile = configuration.baseDirectory / privateKeyAlias val pubIdentityFile = configuration.baseDirectory / "$serviceId-public" - - val identityAndKey = if (configuration.nodeKeystore.exists() && keystore.containsAlias(privateKeyAlias)) { + val certificateAndKeyPair = keyStore.certificateAndKeyPair(privateKeyAlias) + val identityCertPathAndKey: Pair = if (certificateAndKeyPair != null) { + val clientCertPath = keyStore.keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val (cert, keyPair) = certificateAndKeyPair // Get keys from keystore. - val (cert, keyPair) = keystore.getCertificateAndKeyPair(privateKeyAlias, configuration.keyStorePassword) - val loadedServiceName = X509CertificateHolder(cert.encoded).subject - if (X509CertificateHolder(cert.encoded).subject != serviceName) { + val loadedServiceName = cert.subject + if (loadedServiceName != 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) + val certPath = certFactory.generateCertPath(listOf(cert.cert) + clientCertPath) + Pair(PartyAndCertificate(loadedServiceName, keyPair.public, cert, certPath), 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. @@ -627,21 +783,30 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, "$serviceName vs ${myIdentity.name}") // Load the private key. val keyPair = privKeyFile.readAll().deserialize() - val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) - keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) - keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) - Pair(myIdentity, keyPair) + if (myIdentity.owningKey !is CompositeKey) { // TODO: Support case where owningKey is a composite key. + keyStore.save(serviceName, privateKeyAlias, keyPair) + } + val partyAndCertificate = getTestPartyAndCertificate(myIdentity) + // Sanity check the certificate and path + val validatorParameters = PKIXParameters(setOf(TrustAnchor(DUMMY_CA.certificate.cert, null))) + val validator = CertPathValidator.getInstance("PKIX") + validatorParameters.isRevocationEnabled = false + validator.validate(partyAndCertificate.certPath, validatorParameters) as PKIXCertPathValidatorResult + Pair(partyAndCertificate, keyPair) } else { + val clientCertPath = keyStore.keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA) + val clientCA = keyStore.certificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA)!! // Create new keys and store in keystore. log.info("Identity key not found, generating fresh key!") val keyPair: KeyPair = generateKeyPair() val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public) - keystore.addOrReplaceKey(privateKeyAlias, keyPair.private, configuration.keyStorePassword.toCharArray(), arrayOf(cert, *keystore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) - keystore.save(configuration.nodeKeystore, configuration.keyStorePassword) - Pair(Party(serviceName, keyPair.public), keyPair) + val certPath = certFactory.generateCertPath(listOf(cert.cert) + clientCertPath) + keyStore.save(serviceName, privateKeyAlias, keyPair) + require(certPath.certificates.isNotEmpty()) { "Certificate path cannot be empty" } + Pair(PartyAndCertificate(serviceName, keyPair.public, cert, certPath), keyPair) } - partyKeys += identityAndKey.second - return identityAndKey + partyKeys += identityCertPathAndKey.second + return identityCertPathAndKey } protected open fun generateKeyPair() = cryptoGenerateKeyPair() @@ -660,7 +825,17 @@ abstract class AbstractNode(open val configuration: NodeConfiguration, } } -sealed class ServiceFlowInfo { - data class Core(val factory: (Party, Int) -> FlowLogic<*>) : ServiceFlowInfo() - data class CorDapp(val version: Int, val factory: (Party) -> FlowLogic<*>) : ServiceFlowInfo() +private class KeyStoreWrapper(val keyStore: KeyStore, val storePath: Path, private val storePassword: String) { + constructor(storePath: Path, storePassword: String) : this(KeyStoreUtilities.loadKeyStore(storePath, storePassword), storePath, storePassword) + + fun certificateAndKeyPair(alias: String): CertificateAndKeyPair? { + return if (keyStore.containsAlias(alias)) keyStore.getCertificateAndKeyPair(alias, storePassword) else null + } + + fun save(serviceName: X500Name, privateKeyAlias: String, keyPair: KeyPair) { + val clientCA = keyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA, storePassword) + val cert = X509Utilities.createCertificate(CertificateType.IDENTITY, clientCA.certificate, clientCA.keyPair, serviceName, keyPair.public).cert + keyStore.addOrReplaceKey(privateKeyAlias, keyPair.private, storePassword.toCharArray(), arrayOf(cert, *keyStore.getCertificateChain(X509Utilities.CORDA_CLIENT_CA))) + keyStore.save(storePath, storePassword) + } } 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 acdc90af16..b0fced2732 100644 --- a/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt +++ b/node/src/main/kotlin/net/corda/node/internal/CordaRPCOpsImpl.kt @@ -8,6 +8,7 @@ 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.identity.Party import net.corda.core.messaging.* import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache @@ -174,7 +175,8 @@ class CordaRPCOpsImpl( @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 partyFromX500Name(x500Name: X500Name) = services.identityService.partyFromX500Name(x500Name) + override fun partiesFromName(query: String, exactMatch: Boolean): Set = services.identityService.partiesFromName(query, exactMatch) override fun registeredFlows(): List = services.rpcFlows.map { it.name }.sorted() diff --git a/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt b/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt deleted file mode 100644 index c04f898a3b..0000000000 --- a/node/src/main/kotlin/net/corda/node/internal/EnforceSingleNodeIsRunning.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.corda.node.internal - -import net.corda.core.internal.addShutdownHook -import net.corda.core.div -import java.io.RandomAccessFile -import java.lang.management.ManagementFactory -import java.nio.file.Path - -/** - * This function enforces that only a single node is running using the given [baseDirectory] by using a file lock. - */ -fun enforceSingleNodeIsRunning(baseDirectory: Path) { - // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a - // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already - // exists, we try to take the file lock first before replacing it and if that fails it means we're being started - // twice with the same directory: that's a user error and we should bail out. - val pidFile = (baseDirectory / "process-id").toFile() - pidFile.createNewFile() - pidFile.deleteOnExit() - val pidFileRw = RandomAccessFile(pidFile, "rw") - val pidFileLock = pidFileRw.channel.tryLock() - if (pidFileLock == null) { - println("It appears there is already a node running with the specified data directory $baseDirectory") - println("Shut that other node down and try again. It may have process ID ${pidFile.readText()}") - System.exit(1) - } - // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us - // when our process shuts down, but we try in stop() anyway just to be nice. - addShutdownHook { - pidFileLock.release() - } - val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0] - pidFileRw.setLength(0) - pidFileRw.write(ourProcessID.toByteArray()) -} diff --git a/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt new file mode 100644 index 0000000000..06a0a7cc61 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/InitiatedFlowFactory.kt @@ -0,0 +1,27 @@ +package net.corda.node.internal + +import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party +import net.corda.node.services.statemachine.SessionInit + +interface InitiatedFlowFactory> { + fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F + + data class Core>(val factory: (Party, Int) -> F) : InitiatedFlowFactory { + override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F { + return factory(otherParty, platformVersion) + } + } + + data class CorDapp>(val version: Int, val factory: (Party) -> F) : InitiatedFlowFactory { + override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): F { + // TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders + if (sessionInit.flowVerison == version) return factory(otherParty) + throw SessionRejectException( + "Version not supported", + "Version mismatch - ${sessionInit.initiatingFlowClass} is only registered for version $version") + } + } +} + +class SessionRejectException(val rejectMessage: String, val logMessage: String) : Exception() 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 dd4a480190..8a5c469605 100644 --- a/node/src/main/kotlin/net/corda/node/internal/Node.kt +++ b/node/src/main/kotlin/net/corda/node/internal/Node.kt @@ -5,18 +5,18 @@ import com.google.common.net.HostAndPort import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import net.corda.core.* -import net.corda.core.internal.ShutdownHook -import net.corda.core.internal.addShutdownHook +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 @@ -37,15 +37,19 @@ 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 net.corda.nodeapi.internal.ShutdownHook +import net.corda.nodeapi.internal.addShutdownHook 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 org.slf4j.LoggerFactory import java.io.IOException import java.time.Clock import java.util.* import javax.management.ObjectName +import kotlin.system.exitProcess /** * A Node manages a standalone server that takes part in the P2P network. It creates the services found in [ServiceHub], @@ -56,18 +60,32 @@ import javax.management.ObjectName * but nodes are not required to advertise services they run (hence subset). * @param clock The clock used within the node and by all flows etc. */ -class Node(override val configuration: FullNodeConfiguration, - advertisedServices: Set, - val versionInfo: VersionInfo, - clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) { +open class Node(override val configuration: FullNodeConfiguration, + advertisedServices: Set, + val versionInfo: VersionInfo, + clock: Clock = NodeClock()) : AbstractNode(configuration, advertisedServices, clock) { companion object { private val logger = loggerFor() + var renderBasicInfoToConsole = true + + /** Used for useful info that we always want to show, even when not logging to the console */ + fun printBasicNodeInfo(description: String, info: String? = null) { + val msg = if (info == null) description else "${description.padEnd(40)}: $info" + val loggerName = if (renderBasicInfoToConsole) "BasicInfo" else "Main" + LoggerFactory.getLogger(loggerName).info(msg) + } + + internal fun failStartUp(message: String): Nothing { + println(message) + println("Corda will now exit...") + exitProcess(1) + } } override val log: Logger get() = logger override val platformVersion: Int get() = versionInfo.platformVersion override val networkMapAddress: NetworkMapAddress? get() = configuration.networkMapService?.address?.let(::NetworkMapAddress) - override fun makeTransactionVerifierService() = (net as NodeMessagingClient).verifierService + override fun makeTransactionVerifierService() = (network as NodeMessagingClient).verifierService // DISCUSSION // @@ -126,7 +144,7 @@ class Node(override val configuration: FullNodeConfiguration, } } - printBasicNodeInfo("Incoming connection address:", advertisedAddress.toString()) + printBasicNodeInfo("Incoming connection address", advertisedAddress.toString()) val myIdentityOrNullIfNetworkMapService = if (networkMapAddress != null) obtainLegalIdentity().owningKey else null return NodeMessagingClient( @@ -227,13 +245,12 @@ class Node(override val configuration: FullNodeConfiguration, override fun startMessagingService(rpcOps: RPCOps) { // Start up the embedded MQ server messageBroker?.apply { - runOnStop += Runnable { stop() } + runOnStop += this::stop start() } // Start up the MQ client. - val net = net as NodeMessagingClient - net.start(rpcOps, userService) + (network as NodeMessagingClient).start(rpcOps, userService) } /** @@ -250,7 +267,7 @@ class Node(override val configuration: FullNodeConfiguration, RaftValidatingNotaryService.type, RaftNonValidatingNotaryService.type -> with(configuration) { val provider = RaftUniquenessProvider(baseDirectory, notaryNodeAddress!!, notaryClusterAddresses, database, configuration) provider.start() - runOnStop += Runnable { provider.stop() } + runOnStop += provider::stop provider } else -> PersistentUniquenessProvider() @@ -279,7 +296,7 @@ class Node(override val configuration: FullNodeConfiguration, "-tcpAllowOthers", "-tcpDaemon", "-key", "node", databaseName) - runOnStop += Runnable { server.stop() } + runOnStop += server::stop val url = server.start().url printBasicNodeInfo("Database connection url is", "jdbc:h2:$url/node") } @@ -321,7 +338,7 @@ class Node(override val configuration: FullNodeConfiguration, /** Starts a blocking event loop for message dispatch. */ fun run() { - (net as NodeMessagingClient).run(messageBroker!!.serverControl) + (network as NodeMessagingClient).run(messageBroker!!.serverControl) } // TODO: Do we really need setup? diff --git a/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt new file mode 100644 index 0000000000..ed841d6de5 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/internal/NodeStartup.kt @@ -0,0 +1,364 @@ +package net.corda.node.internal + +import com.jcabi.manifests.Manifests +import com.jcraft.jsch.JSch +import com.jcraft.jsch.JSchException +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.node.services.ServiceInfo +import net.corda.core.utilities.Emoji +import net.corda.core.utilities.loggerFor +import net.corda.node.* +import net.corda.node.serialization.NodeClock +import net.corda.node.services.config.FullNodeConfiguration +import net.corda.node.services.config.RelayConfiguration +import net.corda.node.services.transactions.bftSMaRtSerialFilter +import net.corda.node.shell.InteractiveShell +import net.corda.node.utilities.TestClock +import net.corda.node.utilities.registration.HTTPNetworkRegistrationService +import net.corda.node.utilities.registration.NetworkRegistrationHelper +import net.corda.nodeapi.internal.addShutdownHook +import org.fusesource.jansi.Ansi +import org.fusesource.jansi.AnsiConsole +import org.slf4j.bridge.SLF4JBridgeHandler +import sun.misc.VMSupport +import java.io.IOException +import java.io.RandomAccessFile +import java.lang.management.ManagementFactory +import java.net.InetAddress +import java.nio.file.Path +import java.nio.file.Paths +import java.util.* +import kotlin.system.exitProcess + +/** This class is responsible for starting a Node from command line arguments. */ +open class NodeStartup(val args: Array) { + companion object { + private val logger by lazy { loggerFor() } + val LOGS_DIRECTORY_NAME = "logs" + val LOGS_CAN_BE_FOUND_IN_STRING = "Logs can be found in" + } + + open fun run() { + val startTime = System.currentTimeMillis() + assertCanNormalizeEmptyPath() + val (argsParser, cmdlineOptions) = parseArguments() + + // We do the single node check before we initialise logging so that in case of a double-node start it + // doesn't mess with the running node's logs. + enforceSingleNodeIsRunning(cmdlineOptions.baseDirectory) + + initLogging(cmdlineOptions) + + val versionInfo = getVersionInfo() + + if (cmdlineOptions.isVersion) { + println("${versionInfo.vendor} ${versionInfo.releaseVersion}") + println("Revision ${versionInfo.revision}") + println("Platform Version ${versionInfo.platformVersion}") + exitProcess(0) + } + + // Maybe render command line help. + if (cmdlineOptions.help) { + argsParser.printHelp(System.out) + exitProcess(0) + } + + drawBanner(versionInfo) + Node.printBasicNodeInfo(LOGS_CAN_BE_FOUND_IN_STRING, System.getProperty("log-path")) + val conf = loadConfigFile(cmdlineOptions) + banJavaSerialisation(conf) + // TODO: Move this to EnterpriseNode.Startup + conf.relay?.let { connectToRelay(it, conf.p2pAddress.port) } + preNetworkRegistration() + maybeRegisterWithNetworkAndExit(cmdlineOptions, conf) + logStartupInfo(versionInfo, cmdlineOptions, conf) + + try { + cmdlineOptions.baseDirectory.createDirectories() + startNode(conf, versionInfo, startTime, cmdlineOptions) + } catch (e: Exception) { + if (e.message?.startsWith("Unknown named curve:") ?: false) { + logger.error("Exception during node startup - ${e.message}. " + + "This is a known OpenJDK issue on some Linux distributions, please use OpenJDK from zulu.org or Oracle JDK.") + } else + logger.error("Exception during node startup", e) + exitProcess(1) + } + + exitProcess(0) + } + + open protected fun preNetworkRegistration() = Unit + + open protected fun createNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, services: Set): Node { + return Node(conf, services, versionInfo, if (conf.useTestClock) TestClock() else NodeClock()) + } + + open protected fun startNode(conf: FullNodeConfiguration, versionInfo: VersionInfo, startTime: Long, cmdlineOptions: CmdLineOptions) { + val advertisedServices = conf.calculateServices() + val node = createNode(conf, versionInfo, advertisedServices) + node.start() + printPluginsAndServices(node) + + node.networkMapRegistrationFuture.success { + val elapsed = (System.currentTimeMillis() - startTime) / 10 / 100.0 + // 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 + Node.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 + node.startupComplete then { + try { + InteractiveShell.startShell(cmdlineOptions.baseDirectory, runShell, cmdlineOptions.sshdServer, node) + } catch(e: Throwable) { + logger.error("Shell failed to start", e) + } + } + } failure { + logger.error("Error during network map registration", it) + exitProcess(1) + } + node.run() + } + + open protected fun logStartupInfo(versionInfo: VersionInfo, cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) { + logger.info("Vendor: ${versionInfo.vendor}") + logger.info("Release: ${versionInfo.releaseVersion}") + logger.info("Platform Version: ${versionInfo.platformVersion}") + logger.info("Revision: ${versionInfo.revision}") + val info = ManagementFactory.getRuntimeMXBean() + logger.info("PID: ${info.name.split("@").firstOrNull()}") // TODO Java 9 has better support for this + logger.info("Main class: ${FullNodeConfiguration::class.java.protectionDomain.codeSource.location.toURI().path}") + logger.info("CommandLine Args: ${info.inputArguments.joinToString(" ")}") + logger.info("Application Args: ${args.joinToString(" ")}") + logger.info("bootclasspath: ${info.bootClassPath}") + logger.info("classpath: ${info.classPath}") + logger.info("VM ${info.vmName} ${info.vmVendor} ${info.vmVersion}") + logger.info("Machine: ${lookupMachineNameAndMaybeWarn()}") + logger.info("Working Directory: ${cmdlineOptions.baseDirectory}") + val agentProperties = VMSupport.getAgentProperties() + if (agentProperties.containsKey("sun.jdwp.listenerAddress")) { + logger.info("Debug port: ${agentProperties.getProperty("sun.jdwp.listenerAddress")}") + } + logger.info("Starting as node on ${conf.p2pAddress}") + } + + open protected fun maybeRegisterWithNetworkAndExit(cmdlineOptions: CmdLineOptions, conf: FullNodeConfiguration) { + if (!cmdlineOptions.isRegistration) return + println() + println("******************************************************************") + println("* *") + println("* Registering as a new participant with Corda network *") + println("* *") + println("******************************************************************") + NetworkRegistrationHelper(conf, HTTPNetworkRegistrationService(conf.certificateSigningService)).buildKeystore() + exitProcess(0) + } + + open protected fun loadConfigFile(cmdlineOptions: CmdLineOptions): FullNodeConfiguration { + val conf = try { + cmdlineOptions.loadConfig() + } catch (e: ConfigException) { + println("Unable to load the configuration file: ${e.rootCause.message}") + exitProcess(2) + } + return conf + } + + open protected fun banJavaSerialisation(conf: FullNodeConfiguration) { + SerialFilter.install(if (conf.bftReplicaId != null) ::bftSMaRtSerialFilter else ::defaultSerialFilter) + } + + open protected fun getVersionInfo(): VersionInfo { + // Manifest properties are only available if running from the corda jar + fun manifestValue(name: String): String? = if (Manifests.exists(name)) Manifests.read(name) else null + + val versionInfo = VersionInfo( + manifestValue("Corda-Platform-Version")?.toInt() ?: 1, + manifestValue("Corda-Release-Version") ?: "Unknown", + manifestValue("Corda-Revision") ?: "Unknown", + manifestValue("Corda-Vendor") ?: "Unknown" + ) + return versionInfo + } + + private fun enforceSingleNodeIsRunning(baseDirectory: Path) { + // Write out our process ID (which may or may not resemble a UNIX process id - to us it's just a string) to a + // file that we'll do our best to delete on exit. But if we don't, it'll be overwritten next time. If it already + // exists, we try to take the file lock first before replacing it and if that fails it means we're being started + // twice with the same directory: that's a user error and we should bail out. + val pidFile = (baseDirectory / "process-id").toFile() + pidFile.createNewFile() + pidFile.deleteOnExit() + val pidFileRw = RandomAccessFile(pidFile, "rw") + val pidFileLock = pidFileRw.channel.tryLock() + if (pidFileLock == null) { + println("It appears there is already a node running with the specified data directory $baseDirectory") + println("Shut that other node down and try again. It may have process ID ${pidFile.readText()}") + System.exit(1) + } + // Avoid the lock being garbage collected. We don't really need to release it as the OS will do so for us + // when our process shuts down, but we try in stop() anyway just to be nice. + addShutdownHook { + pidFileLock.release() + } + val ourProcessID: String = ManagementFactory.getRuntimeMXBean().name.split("@")[0] + pidFileRw.setLength(0) + pidFileRw.write(ourProcessID.toByteArray()) + } + + private fun parseArguments(): Pair { + val argsParser = ArgsParser() + val cmdlineOptions = try { + argsParser.parse(*args) + } catch (ex: OptionException) { + println("Invalid command line arguments: ${ex.message}") + argsParser.printHelp(System.out) + exitProcess(1) + } + return Pair(argsParser, cmdlineOptions) + } + + open protected fun initLogging(cmdlineOptions: CmdLineOptions) { + val loggingLevel = cmdlineOptions.loggingLevel.name.toLowerCase(Locale.ENGLISH) + System.setProperty("defaultLogLevel", loggingLevel) // These properties are referenced from the XML config file. + if (cmdlineOptions.logToConsole) { + System.setProperty("consoleLogLevel", loggingLevel) + Node.renderBasicInfoToConsole = false + } + System.setProperty("log-path", (cmdlineOptions.baseDirectory / LOGS_DIRECTORY_NAME).toString()) + SLF4JBridgeHandler.removeHandlersForRootLogger() // The default j.u.l config adds a ConsoleHandler. + SLF4JBridgeHandler.install() + } + + private fun lookupMachineNameAndMaybeWarn(): String { + val start = System.currentTimeMillis() + val hostName: String = InetAddress.getLocalHost().hostName + val elapsed = System.currentTimeMillis() - start + if (elapsed > 1000 && hostName.endsWith(".local")) { + // User is probably on macOS and experiencing this problem: http://stackoverflow.com/questions/10064581/how-can-i-eliminate-slow-resolving-loading-of-localhost-virtualhost-a-2-3-secon + // + // Also see https://bugs.openjdk.java.net/browse/JDK-8143378 + val messages = listOf( + "Your computer took over a second to resolve localhost due an incorrect configuration. Corda will work but start very slowly until this is fixed. ", + "Please see https://docs.corda.net/troubleshooting.html#slow-localhost-resolution for information on how to fix this. ", + "It will only take a few seconds for you to resolve." + ) + logger.warn(messages.joinToString("")) + Emoji.renderIfSupported { + print(Ansi.ansi().fgBrightRed()) + messages.forEach { + println("${Emoji.sleepingFace}$it") + } + print(Ansi.ansi().reset()) + } + } + return hostName + } + + private fun assertCanNormalizeEmptyPath() { + // Check we're not running a version of Java with a known bug: https://github.com/corda/corda/issues/83 + try { + Paths.get("").normalize() + } catch (e: ArrayIndexOutOfBoundsException) { + Node.failStartUp("You are using a version of Java that is not supported (${System.getProperty("java.version")}). Please upgrade to the latest version.") + } + } + + private fun printPluginsAndServices(node: Node) { + node.configuration.extraAdvertisedServiceIds.let { + if (it.isNotEmpty()) Node.printBasicNodeInfo("Providing network services", it.joinToString()) + } + val plugins = node.pluginRegistries + .map { it.javaClass.name } + .filterNot { it.startsWith("net.corda.node.") || it.startsWith("net.corda.core.") || it.startsWith("net.corda.nodeapi.") } + .map { it.substringBefore('$') } + if (plugins.isNotEmpty()) + Node.printBasicNodeInfo("Loaded plugins", plugins.joinToString()) + } + + open fun drawBanner(versionInfo: VersionInfo) { + // This line makes sure ANSI escapes work on Windows, where they aren't supported out of the box. + AnsiConsole.systemInstall() + + Emoji.renderIfSupported { + val messages = arrayListOf( + "The only distributed ledger that pays\nhomage to Pac Man in its logo.", + "You know, I was a banker\nonce ... but I lost interest. ${Emoji.bagOfCash}", + "It's not who you know, it's who you know\nknows what you know you know.", + "It runs on the JVM because QuickBasic\nis apparently not 'professional' enough.", + "\"It's OK computer, I go to sleep after\ntwenty minutes of inactivity too!\"", + "It's kind of like a block chain but\ncords sounded healthier than chains.", + "Computer science and finance together.\nYou should see our crazy Christmas parties!", + "I met my bank manager yesterday and asked\nto check my balance ... he pushed me over!", + "A banker with nobody around may find\nthemselves .... a-loan! ", + "Whenever I go near my bank I get\nwithdrawal symptoms ${Emoji.coolGuy}", + "There was an earthquake in California,\na local bank went into de-fault.", + "I asked for insurance if the nearby\nvolcano erupted. They said I'd be covered.", + "I had an account with a bank in the\nNorth Pole, but they froze all my assets ${Emoji.santaClaus}", + "Check your contracts carefully. The fine print\nis usually a clause for suspicion ${Emoji.santaClaus}", + "Some bankers are generous ...\nto a vault! ${Emoji.bagOfCash} ${Emoji.coolGuy}", + "What you can buy for a dollar these\ndays is absolute non-cents! ${Emoji.bagOfCash}", + "Old bankers never die, they\njust... pass the buck", + "I won $3M on the lottery so I donated a quarter\nof it to charity. Now I have $2,999,999.75.", + "There are two rules for financial success:\n1) Don't tell everything you know.", + "Top tip: never say \"oops\", instead\nalways say \"Ah, Interesting!\"", + "Computers are useless. They can only\ngive you answers. -- Picasso" + ) + if (Emoji.hasEmojiTerminal) + messages += "Kind of like a regular database but\nwith emojis, colours and ascii art. ${Emoji.coolGuy}" + val (msg1, msg2) = messages.randomOrNull()!!.split('\n') + + println(Ansi.ansi().fgBrightRed().a(""" +______ __ +/ ____/ _________/ /___ _ +/ / __ / ___/ __ / __ `/ """).fgBrightBlue().a(msg1).newline().fgBrightRed().a( + "/ /___ /_/ / / / /_/ / /_/ / ").fgBrightBlue().a(msg2).newline().fgBrightRed().a( + """\____/ /_/ \__,_/\__,_/""").reset().newline().newline().fgBrightDefault().bold(). + a("--- ${versionInfo.vendor} ${versionInfo.releaseVersion} (${versionInfo.revision.take(7)}) -----------------------------------------------"). + newline(). + newline(). + a("${Emoji.books}New! ").reset().a("Training now available worldwide, see https://corda.net/corda-training/"). + newline(). + reset()) + } + } + + private fun connectToRelay(config: RelayConfiguration, localBrokerPort: Int) { + with(config) { + val jsh = JSch().apply { + val noPassphrase = byteArrayOf() + addIdentity(privateKeyFile.toString(), publicKeyFile.toString(), noPassphrase) + } + + val session = jsh.getSession(username, relayHost, sshPort).apply { + // We don't check the host fingerprints because they may change often + setConfig("StrictHostKeyChecking", "no") + } + + try { + logger.info("Connecting to a relay at $relayHost") + session.connect() + } catch (e: JSchException) { + throw IOException("Unable to establish a SSH connection: $username@$relayHost", e) + } + try { + val localhost = "127.0.0.1" + logger.info("Forwarding ports: $relayHost:$remoteInboundPort -> $localhost:$localBrokerPort") + session.setPortForwardingR(remoteInboundPort, localhost, localBrokerPort) + } catch (e: JSchException) { + throw IOException("Unable to set up port forwarding - is SSH on the remote host configured correctly? " + + "(port forwarding is not enabled by default)", e) + } + } + + logger.info("Relay setup successfully!") + } +} \ No newline at end of file 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 33658c855c..e6b8c0e6e9 100644 --- a/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt +++ b/node/src/main/kotlin/net/corda/node/services/CoreFlowHandlers.kt @@ -5,10 +5,10 @@ import net.corda.core.contracts.ContractState import net.corda.core.contracts.TransactionType import net.corda.core.contracts.UpgradedContract import net.corda.core.contracts.requireThat -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic +import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.* diff --git a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt index 13edf41170..5c974fcfb1 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AbstractNodeService.kt @@ -13,7 +13,7 @@ import javax.annotation.concurrent.ThreadSafe @ThreadSafe abstract class AbstractNodeService(val services: ServiceHubInternal) : SingletonSerializeAsToken() { - val net: MessagingService get() = services.networkService + val network: MessagingService get() = services.networkService /** * Register a handler for a message topic. In comparison to using net.addMessageHandler() this manages a lot of @@ -28,14 +28,14 @@ abstract class AbstractNodeService(val services: ServiceHubInternal) : Singleton addMessageHandler(topic: String, crossinline handler: (Q) -> R, crossinline exceptionConsumer: (Message, Exception) -> Unit): MessageHandlerRegistration { - return net.addMessageHandler(topic, DEFAULT_SESSION_ID) { message, _ -> + return network.addMessageHandler(topic, DEFAULT_SESSION_ID) { message, _ -> try { val request = message.data.deserialize() val response = handler(request) // If the return type R is Unit, then do not send a response if (response.javaClass != Unit.javaClass) { - val msg = net.createMessage(topic, request.sessionID, response.serialize().bytes) - net.send(msg, request.replyTo) + val msg = network.createMessage(topic, request.sessionID, response.serialize().bytes) + network.send(msg, request.replyTo) } } catch(e: Exception) { exceptionConsumer(message, e) diff --git a/node/src/main/kotlin/net/corda/node/services/api/AcceptsFileUpload.kt b/node/src/main/kotlin/net/corda/node/services/api/AcceptsFileUpload.kt index 24d787f27c..a3967ec2ba 100644 --- a/node/src/main/kotlin/net/corda/node/services/api/AcceptsFileUpload.kt +++ b/node/src/main/kotlin/net/corda/node/services/api/AcceptsFileUpload.kt @@ -4,9 +4,8 @@ import net.corda.core.node.services.FileUploader /** * A service that implements AcceptsFileUpload can have new binary data provided to it via an HTTP upload. - * - * TODO: In future, also accept uploads over the MQ interface too. */ +// TODO This is no longer used and can be removed interface AcceptsFileUpload : FileUploader { /** A string that prefixes the URLs, e.g. "attachments" or "interest-rates". Should be OK for URLs. */ val dataTypePrefix: String 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 e1af4b173a..5ad98740a3 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 @@ -4,7 +4,7 @@ import com.google.common.annotations.VisibleForTesting import com.google.common.util.concurrent.ListenableFuture import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.FlowStateMachine import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.PluginServiceHub @@ -13,7 +13,7 @@ import net.corda.core.node.services.TxWritableStorageService import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.loggerFor -import net.corda.node.internal.ServiceFlowInfo +import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.messaging.MessagingService import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -21,21 +21,21 @@ import net.corda.node.services.statemachine.FlowStateMachineImpl interface NetworkMapCacheInternal : NetworkMapCache { /** * Deregister from updates from the given map service. - * @param net the network messaging service. + * @param network the network messaging service. * @param service the network map service to fetch current state from. */ - fun deregisterForUpdates(net: MessagingService, service: NodeInfo): ListenableFuture + fun deregisterForUpdates(network: MessagingService, service: NodeInfo): ListenableFuture /** * Add a network map service; fetches a copy of the latest map from the service and subscribes to any further * updates. - * @param net the network messaging service. + * @param network the network messaging service. * @param networkMapAddress the network map service to fetch current state from. * @param subscribe if the cache should subscribe to updates. * @param ifChangedSinceVer an optional version number to limit updating the map based on. If the latest map * version is less than or equal to the given version, no update is fetched. */ - fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient, + fun addMapService(network: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean, ifChangedSinceVer: Int? = null): ListenableFuture /** Adds a node to the local cache (generally only used for adding ourselves). */ @@ -47,7 +47,6 @@ interface NetworkMapCacheInternal : NetworkMapCache { /** For testing where the network map cache is manipulated marks the service as immediately ready. */ @VisibleForTesting fun runWithoutMapService() - } @CordaSerializable @@ -93,7 +92,6 @@ abstract class ServiceHubInternal : PluginServiceHub { * Starts an already constructed flow. Note that you must be on the server thread to call this method. [FlowInitiator] * defaults to [FlowInitiator.RPC] with username "Only For Testing". */ - // TODO Move it to test utils. @VisibleForTesting fun startFlow(logic: FlowLogic): FlowStateMachine = startFlow(logic, FlowInitiator.RPC("Only For Testing")) @@ -103,7 +101,6 @@ abstract class ServiceHubInternal : PluginServiceHub { */ abstract fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl - /** * Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the flow. * Note that you must be on the server thread to call this method. [flowInitiator] points how flow was started, @@ -122,5 +119,5 @@ abstract class ServiceHubInternal : PluginServiceHub { return startFlow(logic, flowInitiator) } - abstract fun getServiceFlowFactory(clientFlowClass: Class>): ServiceFlowInfo? + abstract fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? } \ No newline at end of file diff --git a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt index 51cd629c7e..fe0fc0c919 100644 --- a/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/config/NodeConfiguration.kt @@ -1,30 +1,22 @@ package net.corda.node.services.config import com.google.common.net.HostAndPort -import net.corda.core.div -import net.corda.core.node.VersionInfo import net.corda.core.node.services.ServiceInfo import net.corda.node.internal.NetworkMapInfo -import net.corda.node.internal.Node -import net.corda.node.serialization.NodeClock import net.corda.node.services.messaging.CertificateChainCheckPolicy import net.corda.node.services.network.NetworkMapService -import net.corda.node.utilities.TestClock import net.corda.nodeapi.User +import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.OldConfig -import net.corda.nodeapi.config.SSLConfiguration import org.bouncycastle.asn1.x500.X500Name import java.net.URL import java.nio.file.Path import java.util.* -interface NodeConfiguration : SSLConfiguration { - val baseDirectory: Path - override val certificatesDirectory: Path get() = baseDirectory / "certificates" +interface NodeConfiguration : NodeSSLConfiguration { val myLegalName: X500Name val networkMapService: NetworkMapInfo? val minimumPlatformVersion: Int - val nearestCity: String val emailAddress: String val exportJMXto: String val dataSourceProperties: Properties @@ -43,7 +35,6 @@ data class FullNodeConfiguration( ReplaceWith("baseDirectory")) val basedir: Path, override val myLegalName: X500Name, - override val nearestCity: String, override val emailAddress: String, override val keyStorePassword: String, override val trustStorePassword: String, @@ -84,14 +75,13 @@ data class FullNodeConfiguration( } } - fun createNode(versionInfo: VersionInfo): Node { + fun calculateServices(): Set { val advertisedServices = extraAdvertisedServiceIds .filter(String::isNotBlank) .map { ServiceInfo.parse(it) } .toMutableSet() if (networkMapService == null) advertisedServices += ServiceInfo(NetworkMapService.type) - - return Node(this, advertisedServices, versionInfo, if (useTestClock) TestClock() else NodeClock()) + return advertisedServices } } diff --git a/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt b/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt index a122022a62..e9652e9556 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt +++ b/node/src/main/kotlin/net/corda/node/services/database/KotlinConfigurationTransactionWrapper.kt @@ -131,7 +131,7 @@ class KotlinConfigurationTransactionWrapper(private val model: EntityModel, override fun getConnection(): Connection { val tx = TransactionManager.manager.currentOrNull() return CordaConnection( - tx?.connection ?: TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ).connection + tx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") ) } } diff --git a/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt b/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt index 908661d837..6bbc6ae0bb 100644 --- a/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt +++ b/node/src/main/kotlin/net/corda/node/services/database/RequeryConfiguration.kt @@ -48,7 +48,7 @@ class RequeryConfiguration(val properties: Properties, val useDefaultLogging: Bo // TODO: remove once Requery supports QUERY WITH COMPOSITE_KEY IN fun jdbcSession(): Connection { val ctx = TransactionManager.manager.currentOrNull() - return ctx?.connection ?: TransactionManager.manager.newTransaction(Connection.TRANSACTION_REPEATABLE_READ).connection + return ctx?.connection ?: throw IllegalStateException("Was expecting to find database transaction: must wrap calling code within a transaction.") } } 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 25ebc637a4..ff8553bc84 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,22 +1,28 @@ package net.corda.node.services.identity import net.corda.core.contracts.PartyAndReference -import net.corda.core.contracts.requireThat +import net.corda.core.crypto.Crypto +import net.corda.core.crypto.cert +import net.corda.core.crypto.subject 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.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService 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 org.bouncycastle.cert.X509CertificateHolder 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 +import javax.security.auth.x500.X500Principal +import kotlin.collections.ArrayList /** * Simple identity service which caches parties and provides functionality for efficient lookup. @@ -25,15 +31,20 @@ import javax.annotation.concurrent.ThreadSafe * @param certPaths initial set of certificate paths for the service, typically only used for unit tests. */ @ThreadSafe -class InMemoryIdentityService(identities: Iterable = emptySet(), - certPaths: Map = emptyMap()) : SingletonSerializeAsToken(), IdentityService { +class InMemoryIdentityService(identities: Iterable, + certPaths: Map = emptyMap(), + val trustRoot: X509Certificate?) : SingletonSerializeAsToken(), IdentityService { + constructor(identities: Iterable = emptySet(), + certPaths: Map = emptyMap(), + trustRoot: X509CertificateHolder?) : this(identities, certPaths, trustRoot?.cert) companion object { private val log = loggerFor() } - private val keyToParties = ConcurrentHashMap() - private val principalToParties = ConcurrentHashMap() - private val partyToPath = ConcurrentHashMap() + private val trustAnchor: TrustAnchor? = trustRoot?.let { cert -> TrustAnchor(cert, null) } + private val keyToParties = ConcurrentHashMap() + private val principalToParties = ConcurrentHashMap() + private val partyToPath = ConcurrentHashMap() init { keyToParties.putAll(identities.associateBy { it.owningKey } ) @@ -41,51 +52,103 @@ class InMemoryIdentityService(identities: Iterable = emptySet(), partyToPath.putAll(certPaths) } - override fun registerIdentity(party: Party) { + // TODO: Check the certificate validation logic + @Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class) + override fun registerIdentity(party: PartyAndCertificate) { + require(party.certPath.certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" } + // Validate the chain first, before we do anything clever with it + if (trustRoot != null) validateCertificatePath(party.party, party.certPath) + log.trace { "Registering identity $party" } + require(Arrays.equals(party.certificate.subjectPublicKeyInfo.encoded, party.owningKey.encoded)) { "Party certificate must end with party's public key" } + + partyToPath[party.party] = party.certPath keyToParties[party.owningKey] = party principalToParties[party.name] = party } - // We give the caller a copy of the data set to avoid any locking problems - override fun getAllIdentities(): Iterable = ArrayList(keyToParties.values) + override fun certificateFromParty(party: Party): PartyAndCertificate? = principalToParties[party.name] - override fun partyFromKey(key: PublicKey): Party? = keyToParties[key] + // We give the caller a copy of the data set to avoid any locking problems + override fun getAllIdentities(): Iterable = ArrayList(keyToParties.values) + + override fun partyFromKey(key: PublicKey): Party? = keyToParties[key]?.party @Deprecated("Use partyFromX500Name") - override fun partyFromName(name: String): Party? = principalToParties[X500Name(name)] - override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal] - override fun partyFromAnonymous(party: AbstractParty): Party? = partyFromKey(party.owningKey) + override fun partyFromName(name: String): Party? = principalToParties[X500Name(name)]?.party + override fun partyFromX500Name(principal: X500Name): Party? = principalToParties[principal]?.party + override fun partyFromAnonymous(party: AbstractParty) = party as? Party ?: partyFromKey(party.owningKey) override fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) + override fun requirePartyFromAnonymous(party: AbstractParty): Party { + return partyFromAnonymous(party) ?: throw IllegalStateException("Could not deanonymise party ${party.owningKey.toStringShort()}") + } + + override fun partiesFromName(query: String, exactMatch: Boolean): Set { + val results = HashSet() + for ((x500name, partyAndCertificate) in principalToParties) { + val party = partyAndCertificate.party + for (rdn in x500name.rdNs) { + val component = rdn.first.value.toString() + if (exactMatch && component == query) { + results += party + } else if (!exactMatch) { + // We can imagine this being a query over a lucene index in future. + // + // Kostas says: We can easily use the Jaro-Winkler distance metric as it is best suited for short + // strings such as entity/company names, and to detect small typos. We can also apply it for city + // or any keyword related search in lists of records (not raw text - for raw text we need indexing) + // and we can return results in hierarchical order (based on normalised String similarity 0.0-1.0). + if (component.contains(query, ignoreCase = true)) + results += party + } + } + } + return results + } @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}\"" } + require(path.certificates.size > 1) { "Certificate path must contain at least two certificates" } + val actual = path.certificates[1] + require(actual is X509Certificate && actual.publicKey == party.owningKey) { "Next certificate in the path must match the party key ${party.owningKey.toStringShort()}." } + val target = path.certificates.first() + require(target is X509Certificate && target.publicKey == anonymousParty.owningKey) { "Certificate path starts with a certificate for the anonymous party" } } 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) + override fun registerAnonymousIdentity(anonymousParty: AnonymousParty, party: Party, path: CertPath) { + val fullParty = certificateFromParty(party) ?: throw IllegalArgumentException("Unknown identity ${party.name}") 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) + // Validate the chain first, before we do anything clever with it + if (trustRoot != null) validateCertificatePath(anonymousParty, path) + val subjectCertificate = path.certificates.first() + require(subjectCertificate is X509Certificate && subjectCertificate.subject == fullParty.name) { "Subject of the transaction certificate must match the well known identity" } + + log.trace { "Registering identity $fullParty" } partyToPath[anonymousParty] = path + keyToParties[anonymousParty.owningKey] = fullParty + principalToParties[fullParty.name] = fullParty + } + + /** + * Verify that the given certificate path is valid and leads to the owning key of the party. + */ + private fun validateCertificatePath(party: AbstractParty, path: CertPath): PKIXCertPathValidatorResult { + // Check that the path ends with a certificate for the correct party. + val endCertificate = path.certificates.first() + // Ensure the key is in the correct format for comparison. + // TODO: Replace with a Bouncy Castle cert path so we can avoid Sun internal classes appearing unexpectedly. + // For now we have to deal with this potentially being an [X509Key] which is Sun's equivalent to + // [SubjectPublicKeyInfo] but doesn't compare properly with [PublicKey]. + val endKey = Crypto.decodePublicKey(endCertificate.publicKey.encoded) + require(endKey == party.owningKey) { "Certificate path validation must end at owning key ${party.owningKey.toStringShort()}, found ${endKey.toStringShort()}" } + + val validatorParameters = PKIXParameters(setOf(trustAnchor)) + val validator = CertPathValidator.getInstance("PKIX") + validatorParameters.isRevocationEnabled = false + return validator.validate(path, validatorParameters) as PKIXCertPathValidatorResult } } 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 6030118332..fd7074833f 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 @@ -5,11 +5,17 @@ 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.identity.Party +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.operator.ContentSigner import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath import java.util.* import javax.annotation.concurrent.ThreadSafe @@ -25,7 +31,8 @@ import javax.annotation.concurrent.ThreadSafe * etc. */ @ThreadSafe -class E2ETestKeyManagementService(initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { +class E2ETestKeyManagementService(val identityService: IdentityService, + initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { private class InnerState { val keys = HashMap() } @@ -51,6 +58,12 @@ class E2ETestKeyManagementService(initialKeys: Set) : SingletonSerializ return keyPair.public } + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) + } + + private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { return mutex.locked { val pk = publicKey.keys.first { keys.containsKey(it) } diff --git a/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt new file mode 100644 index 0000000000..7f4dff1c5e --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/services/keys/KMSUtils.kt @@ -0,0 +1,49 @@ +package net.corda.node.services.keys + +import net.corda.core.crypto.* +import net.corda.core.identity.AnonymousParty +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.node.services.IdentityService +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.operator.ContentSigner +import java.security.KeyPair +import java.security.PublicKey +import java.security.Security +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.time.Duration +import java.util.* + +/** + * Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding + * [X509Certificate] and adds it to the identity service. + * + * @param identityService issuer service to use when registering the certificate. + * @param subjectPublicKey public key of new identity. + * @param issuer issuer to generate a key and certificate for. Must be an identity this node has the private key for. + * @param issuerSigner a content signer for the issuer. + * @param revocationEnabled whether to check revocation status of certificates in the certificate path. + * @return X.509 certificate and path to the trust root. + */ +fun freshCertificate(identityService: IdentityService, + subjectPublicKey: PublicKey, + issuer: PartyAndCertificate, + issuerSigner: ContentSigner, + revocationEnabled: Boolean = false): Pair { + val issuerCertificate = issuer.certificate + val window = X509Utilities.getCertificateValidityWindow(Duration.ZERO, Duration.ofDays(10 * 365), issuerCertificate) + val ourCertificate = Crypto.createCertificate(CertificateType.IDENTITY, issuerCertificate.subject, issuerSigner, issuer.name, subjectPublicKey, window) + val certFactory = CertificateFactory.getInstance("X509") + val ourCertPath = certFactory.generateCertPath(listOf(ourCertificate.cert) + issuer.certPath.certificates) + identityService.registerAnonymousIdentity(AnonymousParty(subjectPublicKey), + issuer.party, + ourCertPath) + return Pair(issuerCertificate, ourCertPath) +} + +fun getSigner(issuerKeyPair: KeyPair): ContentSigner { + val signatureScheme = Crypto.findSignatureScheme(issuerKeyPair.private) + val provider = Security.getProvider(signatureScheme.providerName) + return ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider) +} 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 643dcdf05d..c1f2c692a5 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 @@ -5,14 +5,19 @@ 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.identity.PartyAndCertificate +import net.corda.core.node.services.IdentityService import net.corda.core.node.services.KeyManagementService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.node.utilities.* +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.operator.ContentSigner import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.statements.InsertStatement import java.security.KeyPair import java.security.PrivateKey import java.security.PublicKey +import java.security.cert.CertPath /** * A persistent re-implementation of [E2ETestKeyManagementService] to support node re-start. @@ -21,7 +26,8 @@ import java.security.PublicKey * * This class needs database transactions to be in-flight during method calls and init. */ -class PersistentKeyManagementService(initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { +class PersistentKeyManagementService(val identityService: IdentityService, + initialKeys: Set) : SingletonSerializeAsToken(), KeyManagementService { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}our_key_pairs") { val publicKey = publicKey("public_key") @@ -62,6 +68,12 @@ class PersistentKeyManagementService(initialKeys: Set) : SingletonSeria return keyPair.public } + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) + } + + private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { return mutex.locked { val pk = publicKey.keys.first { keys.containsKey(it) } 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 eb5df6b7e7..077f3d79d1 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 @@ -13,7 +13,7 @@ import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor -import net.corda.node.printBasicNodeInfo +import net.corda.node.internal.Node import net.corda.node.services.RPCUserService import net.corda.node.services.config.NodeConfiguration import net.corda.node.services.messaging.NodeLoginModule.Companion.NODE_ROLE @@ -47,7 +47,6 @@ import org.apache.activemq.artemis.utils.ConfigurationHelper import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.cert.X509CertificateHolder import rx.Subscription -import sun.security.x509.X509CertImpl import java.io.IOException import java.math.BigInteger import java.security.KeyStore @@ -156,9 +155,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, registerPostQueueDeletionCallback { address, qName -> log.debug { "Queue deleted: $qName for $address" } } } activeMQServer.start() - printBasicNodeInfo("Listening on port", p2pPort.toString()) + Node.printBasicNodeInfo("Listening on port", p2pPort.toString()) if (rpcPort != null) { - printBasicNodeInfo("RPC service listening on port", rpcPort.toString()) + Node.printBasicNodeInfo("RPC service listening on port", rpcPort.toString()) } } @@ -263,10 +262,9 @@ class ArtemisMessagingServer(override val config: NodeConfiguration, val trustStore = KeyStoreUtilities.loadKeyStore(config.trustStoreFile, config.trustStorePassword) val ourCertificate = keyStore.getX509Certificate(CORDA_CLIENT_TLS) - val ourSubjectDN = X500Name(ourCertificate.subjectDN.name) // This is a sanity check and should not fail unless things have been misconfigured - require(ourSubjectDN == config.myLegalName) { - "Legal name does not match with our subject CN: $ourSubjectDN" + require(ourCertificate.subject == config.myLegalName) { + "Legal name does not match with our subject CN: ${ourCertificate.subject}" } val defaultCertPolicies = mapOf( PEER_ROLE to CertificateChainCheckPolicy.RootMustMatch, @@ -510,7 +508,7 @@ private class VerifyingNettyConnector(configuration: MutableMap, "Peer has wrong subject name in the certificate - expected $expectedLegalName but got ${peerCertificate.subject}. This is either a fatal " + "misconfiguration by the remote peer or an SSL man-in-the-middle attack!" } - X509Utilities.validateCertificateChain(X509CertImpl(session.localCertificates.last().encoded), *session.peerCertificates) + X509Utilities.validateCertificateChain(X509CertificateHolder(session.localCertificates.last().encoded), *session.peerCertificates) server.onTcpConnection(peerLegalName) } catch (e: IllegalArgumentException) { connection.close() 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 81935366a9..e001d4a248 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 @@ -40,6 +40,7 @@ import rx.Observable import rx.Subscriber import rx.Subscription import java.lang.reflect.InvocationTargetException +import java.lang.reflect.Method import java.time.Duration import java.util.* import java.util.concurrent.* @@ -92,7 +93,7 @@ class RPCServer( } private val lifeCycle = LifeCycle(State.UNSTARTED) // The methodname->Method map to use for dispatching. - private val methodTable = ops.javaClass.declaredMethods.groupBy { it.name }.mapValues { it.value.single() } + private val methodTable: Map // The observable subscription mapping. private val observableMap = createObservableSubscriptionMap() // A mapping from client addresses to IDs of associated Observables @@ -114,6 +115,16 @@ class RPCServer( private var clientBindingRemovalConsumer: ClientConsumer? = null private var serverControl: ActiveMQServerControl? = null + init { + val groupedMethods = ops.javaClass.declaredMethods.groupBy { it.name } + groupedMethods.forEach { name, methods -> + if (methods.size > 1) { + throw IllegalArgumentException("Encountered more than one method called ${name} on ${ops.javaClass.name}") + } + } + methodTable = groupedMethods.mapValues { it.value.single() } + } + private fun createObservableSubscriptionMap(): ObservableSubscriptionMap { val onObservableRemove = RemovalListener { log.debug { "Unsubscribing from Observable with id ${it.key} because of ${it.cause}" } diff --git a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt index ced6e6b972..e1b2664b90 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/InMemoryNetworkMapCache.kt @@ -61,7 +61,7 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach } for ((_, value) in registeredNodes) { for (service in value.advertisedServices) { - if (service.identity == party) { + if (service.identity.party == party) { return PartyInfo.Service(service) } } @@ -77,16 +77,16 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach } } - override fun addMapService(net: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean, + override fun addMapService(network: MessagingService, networkMapAddress: SingleMessageRecipient, subscribe: Boolean, ifChangedSinceVer: Int?): ListenableFuture { if (subscribe && !registeredForPush) { // Add handler to the network, for updates received from the remote network map service. - net.addMessageHandler(NetworkMapService.PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> + network.addMessageHandler(NetworkMapService.PUSH_TOPIC, DEFAULT_SESSION_ID) { message, _ -> try { val req = message.data.deserialize() - val ackMessage = net.createMessage(NetworkMapService.PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, - NetworkMapService.UpdateAcknowledge(req.mapVersion, net.myAddress).serialize().bytes) - net.send(ackMessage, req.replyTo) + val ackMessage = network.createMessage(NetworkMapService.PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, + NetworkMapService.UpdateAcknowledge(req.mapVersion, network.myAddress).serialize().bytes) + network.send(ackMessage, req.replyTo) processUpdatePush(req) } catch(e: NodeMapError) { logger.warn("Failure during node map update due to bad update: ${e.javaClass.name}") @@ -98,8 +98,8 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach } // Fetch the network map and register for updates at the same time - val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVer, net.myAddress) - val future = net.sendRequest(NetworkMapService.FETCH_TOPIC, req, networkMapAddress).map { (nodes) -> + val req = NetworkMapService.FetchMapRequest(subscribe, ifChangedSinceVer, network.myAddress) + val future = network.sendRequest(NetworkMapService.FETCH_TOPIC, req, networkMapAddress).map { (nodes) -> // We may not receive any nodes back, if the map hasn't changed since the version specified nodes?.forEach { processRegistration(it) } Unit @@ -131,10 +131,10 @@ open class InMemoryNetworkMapCache : SingletonSerializeAsToken(), NetworkMapCach * Unsubscribes from updates from the given map service. * @param service the network map service to listen to updates from. */ - override fun deregisterForUpdates(net: MessagingService, service: NodeInfo): ListenableFuture { + override fun deregisterForUpdates(network: MessagingService, service: NodeInfo): ListenableFuture { // Fetch the network map and register for updates at the same time - val req = NetworkMapService.SubscribeRequest(false, net.myAddress) - val future = net.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, service.address).map { + val req = NetworkMapService.SubscribeRequest(false, network.myAddress) + val future = network.sendRequest(NetworkMapService.SUBSCRIPTION_TOPIC, req, service.address).map { if (it.confirmed) Unit else throw NetworkCacheError.DeregistrationFailed() } _registrationFuture.setFuture(future) 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 144223d4ca..035a1a506c 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 @@ -2,8 +2,10 @@ package net.corda.node.services.network import com.google.common.annotations.VisibleForTesting import net.corda.core.ThreadBox -import net.corda.core.crypto.* -import net.corda.core.identity.Party +import net.corda.core.crypto.DigitalSignature +import net.corda.core.crypto.SignedData +import net.corda.core.crypto.isFulfilledBy +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo @@ -16,6 +18,7 @@ import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.node.services.api.AbstractNodeService import net.corda.node.services.api.ServiceHubInternal @@ -80,7 +83,7 @@ interface NetworkMapService { @CordaSerializable data class FetchMapResponse(val nodes: List?, val version: Int) - data class QueryIdentityRequest(val identity: Party, + data class QueryIdentityRequest(val identity: PartyAndCertificate, override val replyTo: SingleMessageRecipient, override val sessionID: Long = random63BitValue()) : ServiceRequestMessage @@ -114,7 +117,7 @@ interface NetworkMapService { class InMemoryNetworkMapService(services: ServiceHubInternal, minimumPlatformVersion: Int) : AbstractNetworkMapService(services, minimumPlatformVersion) { - override val nodeRegistrations: MutableMap = ConcurrentHashMap() + override val nodeRegistrations: MutableMap = ConcurrentHashMap() override val subscribers = ThreadBox(mutableMapOf()) init { @@ -133,14 +136,14 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, val minimumPlatformVersion: Int) : NetworkMapService, AbstractNodeService(services) { companion object { /** - * Maximum credible size for a registration request. Generally requests are around 500-600 bytes, so this gives a + * Maximum credible size for a registration request. Generally requests are around 2000-6000 bytes, so this gives a * 10 times overhead. */ - private const val MAX_SIZE_REGISTRATION_REQUEST_BYTES = 5500 + private const val MAX_SIZE_REGISTRATION_REQUEST_BYTES = 40000 private val logger = loggerFor() } - protected abstract val nodeRegistrations: MutableMap + protected abstract val nodeRegistrations: MutableMap // Map from subscriber address, to most recently acknowledged update map version. protected abstract val subscribers: ThreadBox> @@ -169,7 +172,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, handlers += addMessageHandler(QUERY_TOPIC) { req: QueryIdentityRequest -> processQueryRequest(req) } handlers += addMessageHandler(REGISTER_TOPIC) { req: RegistrationRequest -> processRegistrationRequest(req) } handlers += addMessageHandler(SUBSCRIPTION_TOPIC) { req: SubscribeRequest -> processSubscriptionRequest(req) } - handlers += net.addMessageHandler(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID) { message, _ -> + handlers += network.addMessageHandler(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID) { message, _ -> val req = message.data.deserialize() processAcknowledge(req) } @@ -178,7 +181,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, @VisibleForTesting fun unregisterNetworkHandlers() { for (handler in handlers) { - net.removeMessageHandler(handler) + network.removeMessageHandler(handler) } handlers.clear() } @@ -230,7 +233,9 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, } private fun processRegistrationRequest(request: RegistrationRequest): RegistrationResponse { - if (request.wireReg.raw.size > MAX_SIZE_REGISTRATION_REQUEST_BYTES) { + val requestSize = request.wireReg.raw.size + logger.debug { "Received registration request of size: $requestSize" } + if (requestSize > MAX_SIZE_REGISTRATION_REQUEST_BYTES) { return RegistrationResponse("Request is too big") } @@ -250,7 +255,7 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, // in on different threads, there is no risk of a race condition while checking // sequence numbers. val registrationInfo = try { - nodeRegistrations.compute(node.legalIdentity) { _, existing: NodeRegistrationInfo? -> + nodeRegistrations.compute(node.legalIdentityAndCert) { _, existing: NodeRegistrationInfo? -> require(!((existing == null || existing.reg.type == REMOVE) && change.type == REMOVE)) { "Attempting to de-register unknown node" } @@ -284,14 +289,14 @@ abstract class AbstractNetworkMapService(services: ServiceHubInternal, // TODO: Once we have a better established messaging system, we can probably send // to a MessageRecipientGroup that nodes join/leave, rather than the network map // service itself managing the group - val update = NetworkMapService.Update(wireReg, newMapVersion, net.myAddress).serialize().bytes - val message = net.createMessage(PUSH_TOPIC, DEFAULT_SESSION_ID, update) + val update = NetworkMapService.Update(wireReg, newMapVersion, network.myAddress).serialize().bytes + val message = network.createMessage(PUSH_TOPIC, DEFAULT_SESSION_ID, update) subscribers.locked { // Remove any stale subscribers values.removeIf { (mapVersion) -> newMapVersion - mapVersion > maxUnacknowledgedUpdates } // TODO: introduce some concept of time in the condition to avoid unsubscribes when there's a message burst. - keys.forEach { recipient -> net.send(message, recipient) } + keys.forEach { recipient -> network.send(message, recipient) } } } diff --git a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt index 83a2dd12a1..2220d94a19 100644 --- a/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt +++ b/node/src/main/kotlin/net/corda/node/services/network/PersistentNetworkMapService.kt @@ -1,7 +1,7 @@ package net.corda.node.services.network import net.corda.core.ThreadBox -import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.messaging.SingleMessageRecipient import net.corda.node.services.api.ServiceHubInternal import net.corda.node.utilities.* @@ -22,22 +22,25 @@ class PersistentNetworkMapService(services: ServiceHubInternal, minimumPlatformV : AbstractNetworkMapService(services, minimumPlatformVersion) { private object Table : JDBCHashedTable("${NODE_DATABASE_PREFIX}network_map_nodes") { - val nodeParty = party("node_party_name", "node_party_key") + val nodeParty = partyAndCertificate("node_party_name", "node_party_key", "node_party_certificate", "node_party_path") val registrationInfo = blob("node_registration_info") } - override val nodeRegistrations: MutableMap = synchronizedMap(object : AbstractJDBCHashMap(Table, loadOnInit = true) { + override val nodeRegistrations: MutableMap = synchronizedMap(object : AbstractJDBCHashMap(Table, loadOnInit = true) { // TODO: We should understand an X500Name database field type, rather than manually doing the conversion ourselves - override fun keyFromRow(row: ResultRow): Party = Party(X500Name(row[table.nodeParty.name]), row[table.nodeParty.owningKey]) + override fun keyFromRow(row: ResultRow): PartyAndCertificate = PartyAndCertificate(X500Name(row[table.nodeParty.name]), row[table.nodeParty.owningKey], + row[table.nodeParty.certificate], row[table.nodeParty.certPath]) override fun valueFromRow(row: ResultRow): NodeRegistrationInfo = deserializeFromBlob(row[table.registrationInfo]) - override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { + override fun addKeyToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { insert[table.nodeParty.name] = entry.key.name.toString() insert[table.nodeParty.owningKey] = entry.key.owningKey + insert[table.nodeParty.certPath] = entry.key.certPath + insert[table.nodeParty.certificate] = entry.key.certificate } - override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { + override fun addValueToInsert(insert: InsertStatement, entry: Map.Entry, finalizables: MutableList<() -> Unit>) { insert[table.registrationInfo] = serializeToBlob(entry.value, finalizables) } }) diff --git a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt index b92446e44a..4f69c4dc7b 100644 --- a/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt +++ b/node/src/main/kotlin/net/corda/node/services/persistence/NodeAttachmentService.kt @@ -2,6 +2,7 @@ package net.corda.node.services.persistence import com.codahale.metrics.MetricRegistry import com.google.common.annotations.VisibleForTesting +import com.google.common.hash.HashCode import com.google.common.hash.Hashing import com.google.common.hash.HashingInputStream import com.google.common.io.CountingInputStream @@ -24,6 +25,7 @@ import net.corda.node.services.persistence.schemas.AttachmentEntity import net.corda.node.services.persistence.schemas.Models import java.io.ByteArrayInputStream import java.io.FilterInputStream +import java.io.IOException import java.io.InputStream import java.nio.file.FileAlreadyExistsException import java.nio.file.Path @@ -57,16 +59,14 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: } @CordaSerializable - class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : Exception() { - override fun toString() = "File $expected hashed to $actual: corruption in attachment store?" - } + class HashMismatchException(val expected: SecureHash, val actual: SecureHash) : RuntimeException("File $expected hashed to $actual: corruption in attachment store?") /** * Wraps a stream and hashes data as it is read: if the entire stream is consumed, then at the end the hash of - * the read data is compared to the [expected] hash and [HashMismatchException] is thrown by [close] if they didn't - * match. The goal of this is to detect cases where attachments in the store have been tampered with or corrupted - * and no longer match their file name. It won't always work: if we read a zip for our own uses and skip around - * inside it, we haven't read the whole file, so we can't check the hash. But when copying it over the network + * the read data is compared to the [expected] hash and [HashMismatchException] is thrown by either [read] or [close] + * if they didn't match. The goal of this is to detect cases where attachments in the store have been tampered with + * or corrupted and no longer match their file name. It won't always work: if we read a zip for our own uses and skip + * around inside it, we haven't read the whole file, so we can't check the hash. But when copying it over the network * this will provide an additional safety check against user error. */ @VisibleForTesting @CordaSerializable @@ -75,15 +75,51 @@ class NodeAttachmentService(override var storePath: Path, dataSourceProperties: input: InputStream, private val counter: CountingInputStream = CountingInputStream(input), private val stream: HashingInputStream = HashingInputStream(Hashing.sha256(), counter)) : FilterInputStream(stream) { + @Throws(IOException::class) override fun close() { super.close() + validate() + } + // Possibly not used, but implemented anyway to fulfil the [FilterInputStream] contract. + @Throws(IOException::class) + override fun read(): Int { + return super.read().apply { + if (this == -1) { + validate() + } + } + } + + // This is invoked by [InputStreamSerializer], which does NOT close the stream afterwards. + @Throws(IOException::class) + override fun read(b: ByteArray?, off: Int, len: Int): Int { + return super.read(b, off, len).apply { + if (this == -1) { + validate() + } + } + } + + private fun validate() { if (counter.count != expectedSize.toLong()) return - val actual = SecureHash.SHA256(stream.hash().asBytes()) + val actual = SecureHash.SHA256(hash.asBytes()) if (actual != expected) throw HashMismatchException(expected, actual) } + + private var _hash: HashCode? = null // Backing field for hash property + private val hash: HashCode get() { + var h = _hash + return if (h == null) { + h = stream.hash() + _hash = h + h + } else { + h + } + } } private class AttachmentImpl(override val id: SecureHash, dataLoader: () -> ByteArray, private val checkOnLoad: Boolean) : AbstractAttachment(dataLoader), SerializeAsToken { diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt index 309530bb32..0716b52beb 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/FlowStateMachineImpl.kt @@ -6,11 +6,13 @@ import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.strands.Strand import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture +import net.corda.core.DeclaredField.Companion.declaredField import net.corda.core.ErrorOr import net.corda.core.abbreviate -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.* +import net.corda.core.identity.Party +import net.corda.core.internal.FlowStateMachine import net.corda.core.random63BitValue import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker @@ -27,7 +29,6 @@ import org.jetbrains.exposed.sql.Transaction import org.jetbrains.exposed.sql.transactions.TransactionManager import org.slf4j.Logger import org.slf4j.LoggerFactory -import java.lang.reflect.Modifier import java.sql.Connection import java.sql.SQLException import java.util.* @@ -41,11 +42,7 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, override val flowInitiator: FlowInitiator) : Fiber(id.toString(), scheduler), FlowStateMachine { companion object { // Used to work around a small limitation in Quasar. - private val QUASAR_UNBLOCKER = run { - val field = Fiber::class.java.getDeclaredField("SERIALIZER_BLOCKER") - field.isAccessible = true - field.get(null) - } + private val QUASAR_UNBLOCKER = declaredField(Fiber::class, "SERIALIZER_BLOCKER").value /** * Return the current [FlowStateMachineImpl] or null if executing outside of one. @@ -322,9 +319,8 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, logger.trace { "Initiating a new session with $otherParty" } val session = FlowSession(sessionFlow, random63BitValue(), null, FlowSessionState.Initiating(otherParty), retryable) openSessions[Pair(sessionFlow, otherParty)] = session - // We get the top-most concrete class object to cater for the case where the client flow is customised via a sub-class - val clientFlowClass = sessionFlow.topConcreteFlowClass - val sessionInit = SessionInit(session.ourSessionId, clientFlowClass, clientFlowClass.flowVersion, firstPayload) + val (version, initiatingFlowClass) = sessionFlow.javaClass.flowVersionAndInitiatingClass + val sessionInit = SessionInit(session.ourSessionId, initiatingFlowClass, version, firstPayload) sendInternal(session, sessionInit) if (waitForConfirmation) { session.waitForConfirmation() @@ -332,15 +328,6 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, return session } - @Suppress("UNCHECKED_CAST") - private val FlowLogic<*>.topConcreteFlowClass: Class> get() { - var current: Class> = javaClass - while (!Modifier.isAbstract(current.superclass.modifiers)) { - current = current.superclass as Class> - } - return current - } - @Suspendable private fun waitForMessage(receiveRequest: ReceiveRequest): ReceivedSessionMessage { return receiveRequest.suspendAndExpectReceive().confirmReceiveType(receiveRequest) @@ -460,10 +447,19 @@ class FlowStateMachineImpl(override val id: StateMachineRunId, } } -val Class>.flowVersion: Int get() { - val annotation = requireNotNull(getAnnotation(InitiatingFlow::class.java)) { - "$name as the initiating flow must be annotated with ${InitiatingFlow::class.java.name}" +@Suppress("UNCHECKED_CAST") +val Class>.flowVersionAndInitiatingClass: Pair>> get() { + var current: Class<*> = this + var found: Pair>>? = null + while (true) { + val annotation = current.getDeclaredAnnotation(InitiatingFlow::class.java) + if (annotation != null) { + if (found != null) throw IllegalArgumentException("${InitiatingFlow::class.java.name} can only be annotated once") + require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } + found = annotation.version to (current as Class>) + } + current = current.superclass + ?: return found + ?: throw IllegalArgumentException("$name as an initiating flow must be annotated with ${InitiatingFlow::class.java.name}") } - require(annotation.version > 0) { "Flow versions have to be greater or equal to 1" } - return annotation.version } diff --git a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt index dcfb5621b4..9f35c9eace 100644 --- a/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt +++ b/node/src/main/kotlin/net/corda/node/services/statemachine/SessionMessage.kt @@ -15,7 +15,7 @@ import net.corda.core.utilities.UntrustworthyData interface SessionMessage data class SessionInit(val initiatorSessionId: Long, - val clientFlowClass: Class>, + val initiatingFlowClass: Class>, val flowVerison: Int, val firstPayload: Any?) : SessionMessage 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 9e249ef0f0..595ac8dc47 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 @@ -15,14 +15,14 @@ import com.google.common.collect.HashMultimap import com.google.common.util.concurrent.ListenableFuture import io.requery.util.CloseableIterator import net.corda.core.* -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.flows.* +import net.corda.core.identity.Party import net.corda.core.serialization.* import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor import net.corda.core.utilities.trace -import net.corda.node.internal.ServiceFlowInfo +import net.corda.node.internal.SessionRejectException import net.corda.node.services.api.Checkpoint import net.corda.node.services.api.CheckpointStorage import net.corda.node.services.api.ServiceHubInternal @@ -36,6 +36,7 @@ import rx.subjects.PublishSubject import java.util.* import java.util.concurrent.ConcurrentHashMap import javax.annotation.concurrent.ThreadSafe +import kotlin.collections.ArrayList /** * A StateMachineManager is responsible for coordination and persistence of multiple [FlowStateMachine] objects. @@ -61,7 +62,6 @@ import javax.annotation.concurrent.ThreadSafe */ @ThreadSafe class StateMachineManager(val serviceHub: ServiceHubInternal, - tokenizableServices: List, val checkpointStorage: CheckpointStorage, val executor: AffinityExecutor, val database: Database, @@ -146,8 +146,11 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, private val openSessions = ConcurrentHashMap() private val recentlyClosedSessions = ConcurrentHashMap() + internal val tokenizableServices = ArrayList() // Context for tokenized services in checkpoints - private val serializationContext = SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub) + private val serializationContext by lazy { + SerializeAsTokenContext(tokenizableServices, quasarKryoPool, serviceHub) + } /** Returns a list of all state machines executing the given flow logic at the top level (subflows do not count) */ fun

, T> findStateMachines(flowClass: Class

): List>> { @@ -345,28 +348,15 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, fun sendSessionReject(message: String) = sendSessionMessage(sender, SessionReject(otherPartySessionId, message)) - val serviceFlowInfo = serviceHub.getServiceFlowFactory(sessionInit.clientFlowClass) - if (serviceFlowInfo == null) { - logger.warn("${sessionInit.clientFlowClass} has not been registered with a service flow: $sessionInit") - sendSessionReject("Don't know ${sessionInit.clientFlowClass.name}") + val initiatedFlowFactory = serviceHub.getFlowFactory(sessionInit.initiatingFlowClass) + if (initiatedFlowFactory == null) { + logger.warn("${sessionInit.initiatingFlowClass} has not been registered: $sessionInit") + sendSessionReject("${sessionInit.initiatingFlowClass.name} has not been registered") return } val session = try { - val flow = when (serviceFlowInfo) { - is ServiceFlowInfo.CorDapp -> { - // TODO Add support for multiple versions of the same flow when CorDapps are loaded in separate class loaders - if (sessionInit.flowVerison != serviceFlowInfo.version) { - logger.warn("Version mismatch - ${sessionInit.clientFlowClass} is only registered for version " + - "${serviceFlowInfo.version}: $sessionInit") - sendSessionReject("Version not supported") - return - } - serviceFlowInfo.factory(sender) - } - is ServiceFlowInfo.Core -> serviceFlowInfo.factory(sender, receivedMessage.platformVersion) - } - + val flow = initiatedFlowFactory.createFlow(receivedMessage.platformVersion, sender, sessionInit) val fiber = createFiber(flow, FlowInitiator.Peer(sender)) val session = FlowSession(flow, random63BitValue(), sender, FlowSessionState.Initiated(sender, otherPartySessionId)) if (sessionInit.firstPayload != null) { @@ -376,6 +366,10 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, fiber.openSessions[Pair(flow, sender)] = session updateCheckpoint(fiber) session + } catch (e: SessionRejectException) { + logger.warn("${e.logMessage}: $sessionInit") + sendSessionReject(e.rejectMessage) + return } catch (e: Exception) { logger.warn("Couldn't start flow session from $sessionInit", e) sendSessionReject("Unable to establish session") @@ -383,7 +377,7 @@ class StateMachineManager(val serviceHub: ServiceHubInternal, } sendSessionMessage(sender, SessionConfirm(otherPartySessionId, session.ourSessionId), session.fiber) - session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.clientFlowClass.name}" } + session.fiber.logger.debug { "Initiated by $sender using ${sessionInit.initiatingFlowClass.name}" } session.fiber.logger.trace { "Initiated from $sessionInit on $session" } resumeFiber(session.fiber) } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt index fdc85c84d0..4dd60e71a9 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTNonValidatingNotaryService.kt @@ -1,10 +1,12 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable +import com.google.common.util.concurrent.SettableFuture import net.corda.core.crypto.DigitalSignature -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic -import net.corda.core.node.services.TimestampChecker +import net.corda.core.getOrThrow +import net.corda.core.identity.Party +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize import net.corda.core.transactions.FilteredTransaction @@ -14,29 +16,37 @@ import net.corda.core.utilities.unwrap import net.corda.flows.NotaryException import net.corda.node.services.api.ServiceHubInternal import org.jetbrains.exposed.sql.Database -import java.nio.file.Path import kotlin.concurrent.thread /** * A non-validating notary service operated by a group of parties that don't necessarily trust each other. * - * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and timestamp validity. + * A transaction is notarised when the consensus is reached by the cluster on its uniqueness, and time-window validity. */ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, services: ServiceHubInternal, - timestampChecker: TimestampChecker, - serverId: Int, - db: Database, - private val client: BFTSMaRt.Client) : NotaryService { + timeWindowChecker: TimeWindowChecker, + replicaId: Int, + db: Database) : NotaryService { + val client = BFTSMaRt.Client(config, replicaId) // (Ab)use replicaId for clientId. + private val replicaHolder = SettableFuture.create() + init { + // Replica startup must be in parallel with other replicas, otherwise the constructor may not return: val configHandle = config.handle() - thread(name = "BFTSmartServer-$serverId", isDaemon = true) { + thread(name = "BFT SMaRt replica $replicaId init", isDaemon = true) { configHandle.use { - Server(configHandle.path, serverId, db, "bft_smart_notary_committed_states", services, timestampChecker) + replicaHolder.set(Replica(it, replicaId, db, "bft_smart_notary_committed_states", services, timeWindowChecker)) + log.info("BFT SMaRt replica $replicaId is running.") } } } + fun dispose() { + replicaHolder.getOrThrow().dispose() + client.dispose() + } + companion object { val type = SimpleNotaryService.type.getSubType("bft") private val log = loggerFor() @@ -67,12 +77,12 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, } } - private class Server(configHome: Path, - id: Int, - db: Database, - tableName: String, - services: ServiceHubInternal, - timestampChecker: TimestampChecker) : BFTSMaRt.Server(configHome, id, db, tableName, services, timestampChecker) { + private class Replica(config: BFTSMaRtConfig, + replicaId: Int, + db: Database, + tableName: String, + services: ServiceHubInternal, + timeWindowChecker: TimeWindowChecker) : BFTSMaRt.Replica(config, replicaId, db, tableName, services, timeWindowChecker) { override fun executeCommand(command: ByteArray): ByteArray { val request = command.deserialize() @@ -86,7 +96,7 @@ class BFTNonValidatingNotaryService(config: BFTSMaRtConfig, val id = ftx.rootHash val inputs = ftx.filteredLeaves.inputs - validateTimestamp(ftx.filteredLeaves.timestamp) + validateTimeWindow(ftx.filteredLeaves.timeWindow) commitInputStates(inputs, id, callerIdentity) log.debug { "Inputs committed successfully, signing $id" } 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 bc352301be..2a13f0cab4 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 @@ -1,25 +1,31 @@ package net.corda.node.services.transactions +import bftsmart.communication.ServerCommunicationSystem +import bftsmart.communication.client.netty.NettyClientServerCommunicationSystemClientSide +import bftsmart.communication.client.netty.NettyClientServerSession import bftsmart.tom.MessageContext import bftsmart.tom.ServiceProxy import bftsmart.tom.ServiceReplica +import bftsmart.tom.core.TOMLayer import bftsmart.tom.core.messages.TOMMessage import bftsmart.tom.server.defaultservices.DefaultRecoverable import bftsmart.tom.server.defaultservices.DefaultReplier import bftsmart.tom.util.Extractor +import net.corda.core.DeclaredField.Companion.declaredField import net.corda.core.contracts.StateRef -import net.corda.core.contracts.Timestamp +import net.corda.core.contracts.TimeWindow 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.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.serialization.deserialize import net.corda.core.serialization.serialize +import net.corda.core.toTypedArray import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.debug @@ -28,7 +34,7 @@ import net.corda.flows.NotaryError import net.corda.flows.NotaryException import net.corda.node.services.api.ServiceHubInternal import net.corda.node.services.transactions.BFTSMaRt.Client -import net.corda.node.services.transactions.BFTSMaRt.Server +import net.corda.node.services.transactions.BFTSMaRt.Replica import net.corda.node.utilities.JDBCHashMap import net.corda.node.utilities.transaction import org.jetbrains.exposed.sql.Database @@ -37,10 +43,9 @@ import java.util.* /** * Implements a replicated transaction commit log based on the [BFT-SMaRt](https://github.com/bft-smart/library) - * consensus algorithm. Every replica in the cluster is running a [Server] maintaining the state, and a[Client] is used - * to to relay state modification requests to all [Server]s. + * consensus algorithm. Every replica in the cluster is running a [Replica] maintaining the state, and a [Client] is used + * to relay state modification requests to all [Replica]s. */ -// TODO: Write bft-smart host config file based on Corda node configuration. // TODO: Define and document the configuration of the bft-smart cluster. // TODO: Potentially update the bft-smart API for our use case or rebuild client and server from lower level building // blocks bft-smart provides. @@ -49,18 +54,18 @@ import java.util.* // consensus about membership changes). Nodes that join the cluster for the first time or re-join can go through // a "recovering" state and request missing data from their peers. object BFTSMaRt { - /** Sent from [Client] to [Server]. */ + /** Sent from [Client] to [Replica]. */ @CordaSerializable data class CommitRequest(val tx: Any, val callerIdentity: Party) - /** Sent from [Server] to [Client]. */ + /** Sent from [Replica] to [Client]. */ @CordaSerializable sealed class ReplicaResponse { data class Error(val error: NotaryError) : ReplicaResponse() data class Signature(val txSignature: DigitalSignature) : ReplicaResponse() } - /** An aggregate response from all replica ([Server]) replies sent from [Client] back to the calling application. */ + /** An aggregate response from all replica ([Replica]) replies sent from [Client] back to the calling application. */ @CordaSerializable sealed class ClusterResponse { data class Error(val error: NotaryError) : ClusterResponse() @@ -68,15 +73,26 @@ object BFTSMaRt { } class Client(config: BFTSMaRtConfig, private val clientId: Int) : SingletonSerializeAsToken() { - private val configHandle = config.handle() - companion object { private val log = loggerFor() } /** A proxy for communicating with the BFT cluster */ - private val proxy: ServiceProxy by lazy { - configHandle.use { buildProxy(it.path) } + private val proxy = ServiceProxy(clientId, config.path.toString(), buildResponseComparator(), buildExtractor()) + private val sessionTable = (proxy.communicationSystem as NettyClientServerCommunicationSystemClientSide).declaredField>("sessionTable").value + + fun dispose() { + proxy.close() // XXX: Does this do enough? + } + + private fun awaitClientConnectionToCluster() { + // TODO: Hopefully we only need to wait for the client's initial connection to the cluster, and this method can be moved to some startup code. + while (true) { + val inactive = sessionTable.entries.mapNotNull { if (it.value.channel.isActive) null else it.key } + if (inactive.isEmpty()) break + log.info("Client-replica channels not yet active: $clientId to $inactive") + Thread.sleep((inactive.size * 100).toLong()) + } } /** @@ -85,16 +101,10 @@ object BFTSMaRt { */ fun commitTransaction(transaction: Any, otherSide: Party): ClusterResponse { require(transaction is FilteredTransaction || transaction is SignedTransaction) { "Unsupported transaction type: ${transaction.javaClass.name}" } - val request = CommitRequest(transaction, otherSide) - val responseBytes = proxy.invokeOrdered(request.serialize().bytes) - val response = responseBytes.deserialize() - return response - } - - private fun buildProxy(configHome: Path): ServiceProxy { - val comparator = buildResponseComparator() - val extractor = buildExtractor() - return ServiceProxy(clientId, configHome.toString(), comparator, extractor) + awaitClientConnectionToCluster() + val requestBytes = CommitRequest(transaction, otherSide).serialize().bytes + val responseBytes = proxy.invokeOrdered(requestBytes) + return responseBytes.deserialize() } /** A comparator to check if replies from two replicas are the same. */ @@ -136,29 +146,46 @@ object BFTSMaRt { } } + /** ServiceReplica doesn't have any kind of shutdown method, so we add one in this subclass. */ + private class CordaServiceReplica(replicaId: Int, configHome: Path, owner: DefaultRecoverable) : ServiceReplica(replicaId, configHome.toString(), owner, owner, null, DefaultReplier()) { + private val tomLayerField = declaredField(ServiceReplica::class, "tomLayer") + private val csField = declaredField(ServiceReplica::class, "cs") + fun dispose() { + // Half of what restart does: + val tomLayer = tomLayerField.value + tomLayer.shutdown() // Non-blocking. + val cs = csField.value + cs.join() + cs.serversConn.join() + tomLayer.join() + tomLayer.deliveryThread.join() + // TODO: At the cluster level, join all Sender/Receiver threads. + } + } + /** * Maintains the commit log and executes commit commands received from the [Client]. * * The validation logic can be specified by implementing the [executeCommand] method. */ - @Suppress("LeakingThis") - abstract class Server(configHome: Path, - val replicaId: Int, - val db: Database, - tableName: String, - val services: ServiceHubInternal, - val timestampChecker: TimestampChecker) : DefaultRecoverable() { + abstract class Replica(config: BFTSMaRtConfig, + replicaId: Int, + private val db: Database, + tableName: String, + private val services: ServiceHubInternal, + private val timeWindowChecker: TimeWindowChecker) : DefaultRecoverable() { companion object { - private val log = loggerFor() + private val log = loggerFor() } // TODO: Use Requery with proper DB schema instead of JDBCHashMap. // Must be initialised before ServiceReplica is started - val commitLog = db.transaction { JDBCHashMap(tableName) } + private val commitLog = db.transaction { JDBCHashMap(tableName) } + @Suppress("LeakingThis") + private val replica = CordaServiceReplica(replicaId, config.path, this) - init { - // TODO: Looks like this statement is blocking. Investigate the bft-smart node startup. - ServiceReplica(replicaId, configHome.toString(), this, this, null, DefaultReplier()) + fun dispose() { + replica.dispose() } override fun appExecuteUnordered(command: ByteArray, msgCtx: MessageContext): ByteArray? { @@ -166,15 +193,12 @@ object BFTSMaRt { } override fun appExecuteBatch(command: Array, mcs: Array): Array { - val replies = command.zip(mcs) { c, _ -> - executeCommand(c) - } - return replies.toTypedArray() + return Arrays.stream(command).map(this::executeCommand).toTypedArray() } /** * Implement logic to execute the command and commit the transaction to the log. - * Helper methods are provided for transaction processing: [commitInputStates], [validateTimestamp], and [sign]. + * Helper methods are provided for transaction processing: [commitInputStates], [validateTimeWindow], and [sign]. */ abstract fun executeCommand(command: ByteArray): ByteArray? @@ -201,9 +225,9 @@ object BFTSMaRt { } } - protected fun validateTimestamp(t: Timestamp?) { - if (t != null && !timestampChecker.isValid(t)) - throw NotaryException(NotaryError.TimestampInvalid) + protected fun validateTimeWindow(t: TimeWindow?) { + if (t != null && !timeWindowChecker.isValid(t)) + throw NotaryException(NotaryError.TimeWindowInvalid) } protected fun sign(bytes: ByteArray): DigitalSignature.WithKey { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt index f5f2c72931..0643e4aa6a 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/BFTSMaRtConfig.kt @@ -12,27 +12,27 @@ import java.nio.file.Files * Each instance of this class creates such a configHome, accessible via [path]. * The files are deleted on [close] typically via [use], see [PathManager] for details. */ -class BFTSMaRtConfig(replicaAddresses: List) : PathManager(Files.createTempDirectory("bft-smart-config")) { +class BFTSMaRtConfig(private val replicaAddresses: List, debug: Boolean = false) : PathManager(Files.createTempDirectory("bft-smart-config")) { companion object { internal val portIsClaimedFormat = "Port %s is claimed by another replica: %s" } init { - val claimedPorts = mutableSetOf() - replicaAddresses.map { it.port }.forEach { base -> + val claimedPorts = mutableSetOf() + val n = replicaAddresses.size + (0 until n).forEach { replicaId -> // Each replica claims the configured port and the next one: - (0..1).map { base + it }.forEach { port -> + replicaPorts(replicaId).forEach { port -> claimedPorts.add(port) || throw IllegalArgumentException(portIsClaimedFormat.format(port, claimedPorts)) } } configWriter("hosts.config") { replicaAddresses.forEachIndexed { index, address -> // The documentation strongly recommends IP addresses: - println("${index} ${InetAddress.getByName(address.host).hostAddress} ${address.port}") + println("$index ${InetAddress.getByName(address.host).hostAddress} ${address.port}") } } - val n = replicaAddresses.size - val systemConfig = String.format(javaClass.getResource("system.config.printf").readText(), n, maxFaultyReplicas(n)) + val systemConfig = String.format(javaClass.getResource("system.config.printf").readText(), n, maxFaultyReplicas(n), if (debug) 1 else 0, (0 until n).joinToString(",")) configWriter("system.config") { print(systemConfig) } @@ -46,6 +46,11 @@ class BFTSMaRtConfig(replicaAddresses: List) : PathManager(Files.cr } } } + + private fun replicaPorts(replicaId: Int): List { + val base = replicaAddresses[replicaId] + return (0..1).map { HostAndPort.fromParts(base.host, base.port + it) } + } } fun maxFaultyReplicas(clusterSize: Int) = (clusterSize - 1) / 3 diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt index 6cfed03980..1b0b528e21 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/DistributedImmutableMap.kt @@ -68,7 +68,7 @@ class DistributedImmutableMap(val db: Database, tableName: Str } fun size(commit: Commit): Int { - commit.use { commit -> + commit.use { _ -> return db.transaction { map.size } } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt index e8243e3a48..470ad66a8e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NonValidatingNotaryFlow.kt @@ -2,7 +2,7 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.identity.Party -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider import net.corda.core.transactions.FilteredTransaction import net.corda.core.utilities.unwrap @@ -10,8 +10,8 @@ import net.corda.flows.NotaryFlow import net.corda.flows.TransactionParts class NonValidatingNotaryFlow(otherSide: Party, - timestampChecker: TimestampChecker, - uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) { + timeWindowChecker: TimeWindowChecker, + uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { /** * The received transaction is not checked for contract-validity, as that would require fully * resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction @@ -26,6 +26,6 @@ class NonValidatingNotaryFlow(otherSide: Party, it.verify() it } - return TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timestamp) + return TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timeWindow) } -} \ No newline at end of file +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt index 9d6b13f7db..9abef4eb50 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/NotaryService.kt @@ -7,8 +7,9 @@ interface NotaryService { /** * Factory for producing notary service flows which have the corresponding sends and receives as NotaryFlow.Client. - * The first parameter is the client [Party] making the request and the second is the platform version of the client's - * node. Use this version parameter to provide backwards compatibility if the notary flow protocol changes. + * The first parameter is the client [Party] making the request and the second is the platform version + * of the client's node. Use this version parameter to provide backwards compatibility if the notary flow protocol + * changes. */ val serviceFlowFactory: (Party, Int) -> FlowLogic } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt index 886222ea7a..7a031540f9 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/PathManager.kt @@ -1,11 +1,11 @@ package net.corda.node.services.transactions -import net.corda.core.internal.addShutdownHook +import net.corda.nodeapi.internal.addShutdownHook import java.io.Closeable import java.nio.file.Path import java.util.concurrent.atomic.AtomicInteger -internal class DeleteOnExitPath(internal val path: Path) { +private class DeleteOnExitPath(internal val path: Path) { private val shutdownHook = addShutdownHook { dispose() } internal fun dispose() { path.toFile().deleteRecursively() @@ -13,31 +13,31 @@ internal class DeleteOnExitPath(internal val path: Path) { } } -open class PathHandle internal constructor(private val deleteOnExitPath: DeleteOnExitPath, private val handleCounter: AtomicInteger) : Closeable { - val path - get(): Path { - val path = deleteOnExitPath.path - check(handleCounter.get() != 0) { "Defunct path: $path" } - return path - } - - init { - handleCounter.incrementAndGet() - } - - fun handle() = PathHandle(deleteOnExitPath, handleCounter) - - override fun close() { - if (handleCounter.decrementAndGet() == 0) { - deleteOnExitPath.dispose() - } - } -} - /** * An instance of this class is a handle on a temporary [path]. * If necessary, additional handles on the same path can be created using the [handle] method. * The path is (recursively) deleted when [close] is called on the last handle, typically at the end of a [use] expression. * The value of eager cleanup of temporary files is that there are cases when shutdown hooks don't run e.g. SIGKILL. */ -open class PathManager(path: Path) : PathHandle(DeleteOnExitPath(path), AtomicInteger()) +open class PathManager>(path: Path) : Closeable { + private val deleteOnExitPath = DeleteOnExitPath(path) + private val handleCounter = AtomicInteger(1) + val path + get(): Path { + val path = deleteOnExitPath.path + check(handleCounter.get() != 0) { "Defunct path: $path" } + return path + } + + fun handle(): T { + handleCounter.incrementAndGet() + @Suppress("UNCHECKED_CAST") + return this as T + } + + override fun close() { + if (handleCounter.decrementAndGet() == 0) { + deleteOnExitPath.dispose() + } + } +} diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt index fee9df8674..614dfdeb36 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftNonValidatingNotaryService.kt @@ -2,16 +2,16 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker /** A non-validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftNonValidatingNotaryService(val timestampChecker: TimestampChecker, +class RaftNonValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, val uniquenessProvider: RaftUniquenessProvider) : NotaryService { companion object { val type = SimpleNotaryService.type.getSubType("raft") } override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timestampChecker, uniquenessProvider) + NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt index 624df8db65..ff0217d12d 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/RaftValidatingNotaryService.kt @@ -2,16 +2,16 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker /** A validating notary service operated by a group of mutually trusting parties, uses the Raft algorithm to achieve consensus. */ -class RaftValidatingNotaryService(val timestampChecker: TimestampChecker, +class RaftValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, val uniquenessProvider: RaftUniquenessProvider) : NotaryService { companion object { val type = ValidatingNotaryService.type.getSubType("raft") } override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timestampChecker, uniquenessProvider) + ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt index 4e5731d64c..5ac707bc9e 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/SimpleNotaryService.kt @@ -3,17 +3,17 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider /** A simple Notary service that does not perform transaction validation */ -class SimpleNotaryService(val timestampChecker: TimestampChecker, +class SimpleNotaryService(val timeWindowChecker: TimeWindowChecker, val uniquenessProvider: UniquenessProvider) : NotaryService { companion object { val type = ServiceType.notary.getSubType("simple") } override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - NonValidatingNotaryFlow(otherParty, timestampChecker, uniquenessProvider) + NonValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) } } diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt index 61aa033940..d30180db05 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryFlow.kt @@ -3,7 +3,7 @@ package net.corda.node.services.transactions import co.paralleluniverse.fibers.Suspendable import net.corda.core.contracts.TransactionVerificationException import net.corda.core.identity.Party -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.WireTransaction @@ -18,9 +18,9 @@ import java.security.SignatureException * indeed valid. */ class ValidatingNotaryFlow(otherSide: Party, - timestampChecker: TimestampChecker, + timeWindowChecker: TimeWindowChecker, uniquenessProvider: UniquenessProvider) : - NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) { + NotaryFlow.Service(otherSide, timeWindowChecker, uniquenessProvider) { /** * The received transaction is checked for contract-validity, which requires fully resolving it into a * [TransactionForVerification], for which the caller also has to to reveal the whole transaction @@ -32,7 +32,7 @@ class ValidatingNotaryFlow(otherSide: Party, checkSignatures(stx) val wtx = stx.tx validateTransaction(wtx) - return TransactionParts(wtx.id, wtx.inputs, wtx.timestamp) + return TransactionParts(wtx.id, wtx.inputs, wtx.timeWindow) } private fun checkSignatures(stx: SignedTransaction) { diff --git a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt index 60766b085a..72b819e90a 100644 --- a/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt +++ b/node/src/main/kotlin/net/corda/node/services/transactions/ValidatingNotaryService.kt @@ -3,17 +3,17 @@ package net.corda.node.services.transactions import net.corda.core.flows.FlowLogic import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType -import net.corda.core.node.services.TimestampChecker +import net.corda.core.node.services.TimeWindowChecker import net.corda.core.node.services.UniquenessProvider /** A Notary service that validates the transaction chain of the submitted transaction before committing it */ -class ValidatingNotaryService(val timestampChecker: TimestampChecker, +class ValidatingNotaryService(val timeWindowChecker: TimeWindowChecker, val uniquenessProvider: UniquenessProvider) : NotaryService { companion object { val type = ServiceType.notary.getSubType("validating") } override val serviceFlowFactory: (Party, Int) -> FlowLogic = { otherParty, _ -> - ValidatingNotaryFlow(otherParty, timestampChecker, uniquenessProvider) + ValidatingNotaryFlow(otherParty, timeWindowChecker, uniquenessProvider) } } diff --git a/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt new file mode 100644 index 0000000000..0f11a6a547 --- /dev/null +++ b/node/src/main/kotlin/net/corda/node/shell/FlowWatchPrintingSubscriber.kt @@ -0,0 +1,128 @@ +package net.corda.node.shell + +import com.google.common.util.concurrent.SettableFuture +import net.corda.core.ErrorOr +import net.corda.core.crypto.commonName +import net.corda.core.flows.FlowInitiator +import net.corda.core.flows.StateMachineRunId +import net.corda.core.messaging.StateMachineUpdate +import net.corda.core.then +import net.corda.core.transactions.SignedTransaction +import org.crsh.text.Color +import org.crsh.text.Decoration +import org.crsh.text.RenderPrintWriter +import org.crsh.text.ui.LabelElement +import org.crsh.text.ui.TableElement +import org.crsh.text.ui.Overflow +import org.crsh.text.ui.RowElement +import rx.Subscriber + +class FlowWatchPrintingSubscriber(private val toStream: RenderPrintWriter) : Subscriber() { + private val indexMap = HashMap() + private val table = createStateMachinesTable() + val future: SettableFuture = SettableFuture.create() + + init { + // The future is public and can be completed by something else to indicate we don't wish to follow + // anymore (e.g. the user pressing Ctrl-C). + future then { unsubscribe() } + } + + @Synchronized + override fun onCompleted() { + // The observable of state machines will never complete. + future.set(Unit) + } + + @Synchronized + override fun onNext(t: Any?) { + if (t is StateMachineUpdate) { + toStream.cls() + createStateMachinesRow(t) + toStream.print(table) + toStream.println("Waiting for completion or Ctrl-C ... ") + toStream.flush() + } + } + + @Synchronized + override fun onError(e: Throwable) { + toStream.println("Observable completed with an error") + future.setException(e) + } + + private fun stateColor(smmUpdate: StateMachineUpdate): Color { + return when(smmUpdate){ + is StateMachineUpdate.Added -> Color.blue + is StateMachineUpdate.Removed -> smmUpdate.result.match({ Color.green } , { Color.red }) + } + } + + private fun createStateMachinesTable(): TableElement { + val table = TableElement(1,2,1,2).overflow(Overflow.HIDDEN).rightCellPadding(1) + val header = RowElement(true).add("Id", "Flow name", "Initiator", "Status").style(Decoration.bold.fg(Color.black).bg(Color.white)) + table.add(header) + return table + } + + // TODO Add progress tracker? + private fun createStateMachinesRow(smmUpdate: StateMachineUpdate) { + when (smmUpdate) { + is StateMachineUpdate.Added -> { + table.add(RowElement().add( + LabelElement(formatFlowId(smmUpdate.id)), + LabelElement(formatFlowName(smmUpdate.stateMachineInfo.flowLogicClassName)), + LabelElement(formatFlowInitiator(smmUpdate.stateMachineInfo.initiator)), + LabelElement("In progress") + ).style(stateColor(smmUpdate).fg())) + indexMap[smmUpdate.id] = table.rows.size - 1 + } + is StateMachineUpdate.Removed -> { + val idx = indexMap[smmUpdate.id] + if (idx != null) { + val oldRow = table.rows[idx] + val flowNameLabel = oldRow.getCol(1) as LabelElement + val flowInitiatorLabel = oldRow.getCol(2) as LabelElement + table.rows[idx] = RowElement().add( + LabelElement(formatFlowId(smmUpdate.id)), + LabelElement(flowNameLabel.value), + LabelElement(flowInitiatorLabel.value), + LabelElement(formatFlowResult(smmUpdate.result)) + ).style(stateColor(smmUpdate).fg()) + } + } + } + } + + private fun formatFlowName(flowName: String): String { + val camelCaseRegex = Regex("(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])") + val name = flowName.split('.', '$').last() + // Split CamelCase and get rid of "flow" at the end if present. + return camelCaseRegex.split(name).filter { it.compareTo("Flow", true) != 0 }.joinToString(" ") + } + + private fun formatFlowId(flowId: StateMachineRunId): String { + return flowId.toString().removeSurrounding("[","]") + } + + private fun formatFlowInitiator(flowInitiator: FlowInitiator): String { + return when (flowInitiator) { + is FlowInitiator.Scheduled -> flowInitiator.scheduledState.ref.toString() + is FlowInitiator.Shell -> "Shell" // TODO Change when we will have more information on shell user. + is FlowInitiator.Peer -> flowInitiator.party.name.commonName + is FlowInitiator.RPC -> "RPC: " + flowInitiator.username + } + } + + private fun formatFlowResult(flowResult: ErrorOr<*>): String { + fun successFormat(value: Any?): String { + return when(value) { + is SignedTransaction -> "Tx ID: " + value.id.toString() + is kotlin.Unit -> "No return value" + null -> "No return value" + else -> value.toString() + } + } + return flowResult.match({ successFormat(it) }, { it.message ?: it.toString() }) + } +} 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 6ff7a61d67..3290740576 100644 --- a/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt +++ b/node/src/main/kotlin/net/corda/node/shell/InteractiveShell.kt @@ -12,14 +12,14 @@ import com.google.common.util.concurrent.SettableFuture import net.corda.core.* import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.FlowStateMachine import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.StateMachineUpdate 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 -import net.corda.node.printBasicNodeInfo import net.corda.node.services.messaging.CURRENT_RPC_CONTEXT import net.corda.node.services.messaging.RpcContext import net.corda.node.services.statemachine.FlowStateMachineImpl @@ -90,7 +90,7 @@ object InteractiveShell { // to that local copy, as CRaSH is no longer well maintained by the upstream and the SSH plugin // that it comes with is based on a very old version of Apache SSHD which can't handle connections // from newer SSH clients. It also means hooking things up to the authentication system. - printBasicNodeInfo("SSH server access is not fully implemented, sorry.") + Node.printBasicNodeInfo("SSH server access is not fully implemented, sorry.") runSSH = false } @@ -181,7 +181,7 @@ object InteractiveShell { private val yamlInputMapper: ObjectMapper by lazy { // Return a standard Corda Jackson object mapper, configured to use YAML by default and with extra // serializers. - JacksonSupport.createInMemoryMapper(node.services.identityService, YAMLFactory()).apply { + JacksonSupport.createInMemoryMapper(node.services.identityService, YAMLFactory(), true).apply { val rpcModule = SimpleModule() rpcModule.addDeserializer(InputStream::class.java, InputStreamDeserializer) registerModule(rpcModule) @@ -304,6 +304,34 @@ object InteractiveShell { throw NoApplicableConstructor(errors) } + // TODO Filtering on error/success when we will have some sort of flow auditing, for now it doesn't make much sense. + @JvmStatic + fun runStateMachinesView(out: RenderPrintWriter): Any? { + val proxy = node.rpcOps + val (stateMachines, stateMachineUpdates) = proxy.stateMachinesAndUpdates() + val currentStateMachines = stateMachines.map { StateMachineUpdate.Added(it) } + val subscriber = FlowWatchPrintingSubscriber(out) + stateMachineUpdates.startWith(currentStateMachines).subscribe(subscriber) + var result: Any? = subscriber.future + if (result is Future<*>) { + if (!result.isDone) { + out.cls() + out.println("Waiting for completion or Ctrl-C ... ") + out.flush() + } + try { + result = result.get() + } catch (e: InterruptedException) { + Thread.currentThread().interrupt() + } catch (e: ExecutionException) { + throw e.rootCause + } catch (e: InvocationTargetException) { + throw e.rootCause + } + } + return result + } + @JvmStatic fun runRPCFromString(input: List, out: RenderPrintWriter, context: InvocationContext): Any? { val parser = StringToMethodCallParser(CordaRPCOps::class.java, context.attributes["mapper"] as ObjectMapper) diff --git a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt index 28393b3bf5..4ac4d90ada 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/DatabaseSupport.kt @@ -6,7 +6,10 @@ import com.zaxxer.hikari.HikariDataSource import net.corda.core.crypto.SecureHash import net.corda.core.crypto.parsePublicKeyBase58 import net.corda.core.crypto.toBase58String +import net.corda.core.identity.PartyAndCertificate import net.corda.node.utilities.StrandLocalTransactionManager.Boundary +import org.bouncycastle.cert.X509CertificateHolder +import org.h2.jdbc.JdbcBlob import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.TransactionInterface import org.jetbrains.exposed.sql.transactions.TransactionManager @@ -15,8 +18,12 @@ import rx.Subscriber import rx.subjects.PublishSubject import rx.subjects.Subject import rx.subjects.UnicastSubject +import java.io.ByteArrayInputStream import java.io.Closeable import java.security.PublicKey +import java.security.cert.CertPath +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate import java.sql.Connection import java.time.Instant import java.time.LocalDate @@ -275,17 +282,26 @@ fun rx.Observable.wrapWithDatabaseTransaction(db: Database? = null) // Composite columns for use with below Exposed helpers. data class PartyColumns(val name: Column, val owningKey: Column) - +data class PartyAndCertificateColumns(val name: Column, val owningKey: Column, + val certificate: Column, val certPath: Column) data class StateRefColumns(val txId: Column, val index: Column) data class TxnNoteColumns(val txId: Column, val note: Column) /** * [Table] column helpers for use with Exposed, as per [varchar] etc. */ +fun Table.certificate(name: String) = this.registerColumn(name, X509CertificateColumnType) +fun Table.certificatePath(name: String) = this.registerColumn(name, CertPathColumnType) fun Table.publicKey(name: String) = this.registerColumn(name, PublicKeyColumnType) fun Table.secureHash(name: String) = this.registerColumn(name, SecureHashColumnType) -fun Table.party(nameColumnName: String, keyColumnName: String) = PartyColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName)) +fun Table.party(nameColumnName: String, + keyColumnName: String) = PartyColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName)) +fun Table.partyAndCertificate(nameColumnName: String, + keyColumnName: String, + certificateColumnName: String, + pathColumnName: String) = PartyAndCertificateColumns(this.varchar(nameColumnName, length = 255), this.publicKey(keyColumnName), + this.certificate(certificateColumnName), this.certificatePath(pathColumnName)) fun Table.uuidString(name: String) = this.registerColumn(name, UUIDStringColumnType) fun Table.localDate(name: String) = this.registerColumn(name, LocalDateColumnType) fun Table.localDateTime(name: String) = this.registerColumn(name, LocalDateTimeColumnType) @@ -293,6 +309,35 @@ fun Table.instant(name: String) = this.registerColumn(name, InstantColu fun Table.stateRef(txIdColumnName: String, indexColumnName: String) = StateRefColumns(this.secureHash(txIdColumnName), this.integer(indexColumnName)) fun Table.txnNote(txIdColumnName: String, txnNoteColumnName: String) = TxnNoteColumns(this.secureHash(txIdColumnName), this.text(txnNoteColumnName)) +/** + * [ColumnType] for marshalling to/from database on behalf of [X509CertificateHolder]. + */ +object X509CertificateColumnType : ColumnType() { + override fun sqlType(): String = "BLOB" + + override fun valueFromDB(value: Any): Any { + val blob = value as JdbcBlob + return X509CertificateHolder(blob.getBytes(0, blob.length().toInt())) + } + + override fun notNullValueToDB(value: Any): Any = (value as X509CertificateHolder).encoded +} + +/** + * [ColumnType] for marshalling to/from database on behalf of [CertPath]. + */ +object CertPathColumnType : ColumnType() { + private val factory = CertificateFactory.getInstance("X.509") + override fun sqlType(): String = "BLOB" + + override fun valueFromDB(value: Any): Any { + val blob = value as JdbcBlob + return factory.generateCertPath(ByteArrayInputStream(blob.getBytes(0, blob.length().toInt()))) + } + + override fun notNullValueToDB(value: Any): Any = (value as CertPath).encoded +} + /** * [ColumnType] for marshalling to/from database on behalf of [PublicKey]. */ diff --git a/node/src/main/kotlin/net/corda/node/utilities/FiberBox.kt b/node/src/main/kotlin/net/corda/node/utilities/FiberBox.kt index 97f827239a..d930fe206d 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/FiberBox.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/FiberBox.kt @@ -33,8 +33,8 @@ import kotlin.concurrent.withLock * to be temporary. In addition, it's enitrely possible to envisage a time when we want public [net.corda.core.flows.FlowLogic] * implementations to be able to wait for some condition to become true outside of message send/receive. At that point * we may revisit this implementation and indeed the whole model for this, when we understand that requirement more fully. - * */ +// TODO This is no longer used and can be removed class FiberBox(private val content: T, private val lock: Lock = ReentrantLock()) { private var mutated: SettableFuture? = null 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 31d0bce621..6629f7ca26 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/ServiceIdentityGenerator.kt @@ -1,8 +1,9 @@ package net.corda.node.utilities import net.corda.core.crypto.CompositeKey -import net.corda.core.identity.Party +import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.Party import net.corda.core.serialization.serialize import net.corda.core.serialization.storageKryo import net.corda.core.utilities.loggerFor @@ -24,20 +25,25 @@ object ServiceIdentityGenerator { * @param serviceName The legal name of the distributed service. * @param threshold The threshold for the generated group [CompositeKey]. */ - fun generateToDisk(dirs: List, serviceId: String, serviceName: X500Name, threshold: Int = 1) { + // TODO: This needs to write out to the key store, not just files on disk + fun generateToDisk(dirs: List, + serviceId: String, + serviceName: X500Name, + threshold: Int = 1): Party { log.trace { "Generating a group identity \"serviceName\" for nodes: ${dirs.joinToString()}" } - val keyPairs = (1..dirs.size).map { generateKeyPair() } val notaryKey = CompositeKey.Builder().addKeys(keyPairs.map { it.public }).build(threshold) - val notaryParty = Party(serviceName, notaryKey).serialize() - + // Avoid adding complexity! This class is a hack that needs to stay runnable in the gradle environment. + val notaryParty = Party(serviceName, notaryKey) + val notaryPartyBytes = notaryParty.serialize() + val privateKeyFile = "$serviceId-private-key" + val publicKeyFile = "$serviceId-public" keyPairs.zip(dirs) { keyPair, dir -> Files.createDirectories(dir) - val privateKeyFile = "$serviceId-private-key" - val publicKeyFile = "$serviceId-public" - notaryParty.writeToFile(dir.resolve(publicKeyFile)) + notaryPartyBytes.writeToFile(dir.resolve(publicKeyFile)) // Use storageKryo as our whitelist is not available in the gradle build environment: keyPair.serialize(storageKryo()).writeToFile(dir.resolve(privateKeyFile)) } + return notaryParty } } diff --git a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt index 54343b58a3..9701203d45 100644 --- a/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt +++ b/node/src/main/kotlin/net/corda/node/utilities/registration/NetworkRegistrationHelper.kt @@ -6,6 +6,7 @@ import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_CA import net.corda.core.crypto.X509Utilities.CORDA_CLIENT_TLS import net.corda.core.crypto.X509Utilities.CORDA_ROOT_CA import net.corda.node.services.config.NodeConfiguration +import org.bouncycastle.cert.path.CertPath import org.bouncycastle.openssl.jcajce.JcaPEMWriter import org.bouncycastle.util.io.pem.PemObject import java.io.StringWriter @@ -40,7 +41,8 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val selfSignCert = X509Utilities.createSelfSignedCACertificate(config.myLegalName, keyPair) // Save to the key store. - caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(), arrayOf(selfSignCert)) + caKeyStore.addOrReplaceKey(SELF_SIGNED_PRIVATE_KEY, keyPair.private, privateKeyPassword.toCharArray(), + CertPath(arrayOf(selfSignCert))) caKeyStore.save(config.nodeKeystore, keystorePassword) } val keyPair = caKeyStore.getKeyPair(SELF_SIGNED_PRIVATE_KEY, privateKeyPassword) @@ -73,7 +75,8 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: val caCert = caKeyStore.getX509Certificate(CORDA_CLIENT_CA) val sslCert = X509Utilities.createCertificate(CertificateType.TLS, caCert, keyPair, caCert.subject, sslKey.public) val sslKeyStore = KeyStoreUtilities.loadOrCreateKeyStore(config.sslKeystore, keystorePassword) - sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), arrayOf(sslCert, *certificates)) + sslKeyStore.addOrReplaceKey(CORDA_CLIENT_TLS, sslKey.private, privateKeyPassword.toCharArray(), + arrayOf(sslCert.cert, *certificates)) sslKeyStore.save(config.sslKeystore, config.keyStorePassword) println("SSL private key and certificate stored in ${config.sslKeystore}.") // All done, clean up temp files. @@ -118,7 +121,6 @@ class NetworkRegistrationHelper(val config: NodeConfiguration, val certService: println("Certificate signing request with the following information will be submitted to the Corda certificate signing server.") println() println("Legal Name: ${config.myLegalName}") - println("Nearest City: ${config.nearestCity}") println("Email: ${config.emailAddress}") println() println("Public Key: ${keyPair.public}") diff --git a/node/src/main/resources/net/corda/node/services/transactions/system.config.printf b/node/src/main/resources/net/corda/node/services/transactions/system.config.printf index a8388b3c22..f8724f81f9 100644 --- a/node/src/main/resources/net/corda/node/services/transactions/system.config.printf +++ b/node/src/main/resources/net/corda/node/services/transactions/system.config.printf @@ -65,7 +65,7 @@ system.communication.useSignatures = 0 system.communication.useMACs = 1 #Set to 1 if SMaRt should use the standard output to display debug messages, set to 0 if otherwise -system.debug = 0 +system.debug = %s #Print information about the replica when it is shutdown system.shutdownhook = true @@ -109,7 +109,7 @@ system.totalordermulticast.sync_ckp = false #Replicas ID for the initial view, separated by a comma. # The number of replicas in this parameter should be equal to that specified in 'system.servers.num' -system.initial.view = 0,1,2,3 +system.initial.view = %s #The ID of the trust third party (TTP) system.ttp.id = 7002 diff --git a/node/src/main/resources/reference.conf b/node/src/main/resources/reference.conf index 51b4b29675..7d9f8a2b42 100644 --- a/node/src/main/resources/reference.conf +++ b/node/src/main/resources/reference.conf @@ -1,5 +1,4 @@ myLegalName = "Vast Global MegaCorp, Ltd" -nearestCity = "London" emailAddress = "admin@company.com" exportJMXto = "http" keyStorePassword = "cordacadevpass" diff --git a/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt new file mode 100644 index 0000000000..1aaeb038ef --- /dev/null +++ b/node/src/smoke-test/kotlin/net/corda/node/CordappScanningNodeProcessTest.kt @@ -0,0 +1,52 @@ +package net.corda.node + +import net.corda.core.copyToDirectory +import net.corda.core.createDirectories +import net.corda.core.div +import net.corda.core.utilities.ALICE +import net.corda.nodeapi.User +import net.corda.smoketesting.NodeConfig +import net.corda.smoketesting.NodeProcess +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import java.nio.file.Paths +import java.util.concurrent.atomic.AtomicInteger + +class CordappScanningNodeProcessTest { + private companion object { + val user = User("user1", "test", permissions = setOf("ALL")) + val port = AtomicInteger(15100) + } + + private val factory = NodeProcess.Factory() + + private val aliceConfig = NodeConfig( + party = ALICE, + p2pPort = port.andIncrement, + rpcPort = port.andIncrement, + webPort = port.andIncrement, + extraServices = emptyList(), + users = listOf(user) + ) + + @Test + fun `CorDapp jar in plugins directory is scanned`() { + // If the CorDapp jar does't exist then run the smokeTestClasses gradle task + val cordappJar = Paths.get(javaClass.getResource("/trader-demo.jar").toURI()) + val pluginsDir = (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + cordappJar.copyToDirectory(pluginsDir) + + factory.create(aliceConfig).use { + it.connect().use { + // If the CorDapp wasn't scanned then SellerFlow won't have been picked up as an RPC flow + assertThat(it.proxy.registeredFlows()).contains("net.corda.traderdemo.flow.SellerFlow") + } + } + } + + @Test + fun `empty plugins directory`() { + (factory.baseDirectory(aliceConfig) / "plugins").createDirectories() + factory.create(aliceConfig).close() + } +} diff --git a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java index dfeca9c12e..b596868ac2 100644 --- a/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java +++ b/node/src/test/java/net/corda/node/services/vault/VaultQueryJavaTests.java @@ -1,37 +1,50 @@ package net.corda.node.services.vault; -import com.google.common.collect.*; -import kotlin.*; -import net.corda.contracts.asset.*; +import com.google.common.collect.ImmutableSet; +import kotlin.Pair; +import net.corda.contracts.DealState; +import net.corda.contracts.asset.Cash; import net.corda.core.contracts.*; -import net.corda.core.crypto.*; -import net.corda.core.node.services.*; -import net.corda.core.node.services.vault.*; -import net.corda.core.node.services.vault.QueryCriteria.*; -import net.corda.core.serialization.*; -import net.corda.core.transactions.*; -import net.corda.node.services.vault.schemas.*; -import net.corda.testing.node.*; +import net.corda.core.crypto.SecureHash; +import net.corda.core.node.services.Vault; +import net.corda.core.node.services.VaultService; +import net.corda.core.node.services.vault.PageSpecification; +import net.corda.core.node.services.vault.QueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.LinearStateQueryCriteria; +import net.corda.core.node.services.vault.QueryCriteria.VaultQueryCriteria; +import net.corda.core.node.services.vault.Sort; +import net.corda.core.serialization.OpaqueBytes; +import net.corda.core.transactions.SignedTransaction; +import net.corda.core.transactions.WireTransaction; +import net.corda.node.services.vault.schemas.VaultLinearStateEntity; +import net.corda.testing.node.MockServices; import org.bouncycastle.asn1.x500.X500Name; -import org.jetbrains.annotations.*; -import org.jetbrains.exposed.sql.*; -import org.junit.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.exposed.sql.Database; +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; import rx.Observable; -import java.io.*; +import java.io.Closeable; +import java.io.IOException; import java.util.*; -import java.util.stream.*; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; -import static net.corda.contracts.asset.CashKt.*; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER; +import static net.corda.contracts.asset.CashKt.getDUMMY_CASH_ISSUER_KEY; import static net.corda.contracts.testing.VaultFiller.*; -import static net.corda.core.node.services.vault.QueryCriteriaKt.*; -import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.*; -import static net.corda.core.utilities.TestConstants.*; -import static net.corda.node.utilities.DatabaseSupportKt.*; +import static net.corda.core.node.services.vault.QueryCriteriaKt.and; +import static net.corda.core.node.services.vault.QueryCriteriaUtilsKt.getMAX_PAGE_SIZE; +import static net.corda.core.utilities.TestConstants.getDUMMY_NOTARY; +import static net.corda.node.utilities.DatabaseSupportKt.configureDatabase; import static net.corda.node.utilities.DatabaseSupportKt.transaction; -import static net.corda.testing.CoreTestUtils.*; -import static net.corda.testing.node.MockServicesKt.*; -import static org.assertj.core.api.Assertions.*; +import static net.corda.testing.CoreTestUtils.getMEGA_CORP; +import static net.corda.testing.node.MockServicesKt.makeTestDataSourceProperties; +import static org.assertj.core.api.Assertions.assertThat; @Ignore public class VaultQueryJavaTests { @@ -120,7 +133,7 @@ public class VaultQueryJavaTests { fillWithSomeTestLinearStates(services, 10, uid); List dealIds = Arrays.asList("123", "456", "789"); - fillWithSomeTestDeals(services, dealIds, 0); + fillWithSomeTestDeals(services, dealIds); // DOCSTART VaultJavaQueryExample2 Vault.StateStatus status = Vault.StateStatus.CONSUMED; @@ -191,7 +204,7 @@ public class VaultQueryJavaTests { fillWithSomeTestLinearStates(services, 10, uid); List dealIds = Arrays.asList("123", "456", "789"); - fillWithSomeTestDeals(services, dealIds, 0); + fillWithSomeTestDeals(services, dealIds); // DOCSTART VaultJavaQueryExample2 @SuppressWarnings("unchecked") diff --git a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt index da63771508..1a87c42034 100644 --- a/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt +++ b/node/src/test/kotlin/net/corda/node/CordaRPCOpsImplTest.kt @@ -47,7 +47,7 @@ class CordaRPCOpsImplTest { val testJar = "net/corda/node/testing/test.jar" } - lateinit var network: MockNetwork + lateinit var mockNet: MockNetwork lateinit var aliceNode: MockNode lateinit var notaryNode: MockNode lateinit var rpc: CordaRPCOpsImpl @@ -57,10 +57,10 @@ class CordaRPCOpsImplTest { @Before fun setup() { - network = MockNetwork() - val networkMap = network.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - aliceNode = network.createNode(networkMapAddress = networkMap.info.address) - notaryNode = network.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address) + mockNet = MockNetwork() + val networkMap = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + aliceNode = mockNet.createNode(networkMapAddress = networkMap.info.address) + notaryNode = mockNet.createNode(advertisedServices = ServiceInfo(SimpleNotaryService.type), networkMapAddress = networkMap.info.address) rpc = CordaRPCOpsImpl(aliceNode.services, aliceNode.smm, aliceNode.database) CURRENT_RPC_CONTEXT.set(RpcContext(User("user", "pwd", permissions = setOf( startFlowPermission(), @@ -87,7 +87,7 @@ class CordaRPCOpsImplTest { // Tell the monitoring service node to issue some cash val recipient = aliceNode.info.legalIdentity rpc.startFlow(::CashIssueFlow, Amount(quantity, GBP), ref, recipient, notaryNode.info.notaryIdentity) - network.runNetwork() + mockNet.runNetwork() val expectedState = Cash.State(Amount(quantity, Issued(aliceNode.info.legalIdentity.ref(ref), GBP)), @@ -129,11 +129,11 @@ class CordaRPCOpsImplTest { notaryNode.info.notaryIdentity ) - network.runNetwork() + mockNet.runNetwork() rpc.startFlow(::CashPaymentFlow, Amount(100, USD), aliceNode.info.legalIdentity) - network.runNetwork() + mockNet.runNetwork() var issueSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null diff --git a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt index ea16f4dcf5..210bc46e2e 100644 --- a/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt +++ b/node/src/test/kotlin/net/corda/node/InteractiveShellTest.kt @@ -6,17 +6,18 @@ import net.corda.core.contracts.Amount import net.corda.core.crypto.SecureHash import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.FlowStateMachine import net.corda.core.flows.StateMachineRunId import net.corda.core.identity.Party import net.corda.core.node.ServiceHub import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_PUBKEY_1 +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.UntrustworthyData import net.corda.jackson.JacksonSupport import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.shell.InteractiveShell import net.corda.testing.MEGA_CORP +import net.corda.testing.MEGA_CORP_IDENTITY import org.junit.Test import org.slf4j.Logger import java.util.* @@ -33,8 +34,7 @@ class InteractiveShellTest { override fun call() = a } - private val someCorpLegalName = MEGA_CORP.name - private val ids = InMemoryIdentityService().apply { registerIdentity(Party(someCorpLegalName, DUMMY_PUBKEY_1)) } + private val ids = InMemoryIdentityService(listOf(MEGA_CORP_IDENTITY), trustRoot = DUMMY_CA.certificate) private val om = JacksonSupport.createInMemoryMapper(ids, YAMLFactory()) private fun check(input: String, expected: String) { @@ -67,7 +67,7 @@ class InteractiveShellTest { fun flowTooManyParams() = check("b: 12, c: Yo, d: Bar", "") @Test - fun party() = check("party: \"$someCorpLegalName\"", someCorpLegalName.toString()) + fun party() = check("party: \"${MEGA_CORP.name}\"", MEGA_CORP.name.toString()) class DummyFSM(val logic: FlowA) : FlowStateMachine { override fun sendAndReceive(receiveType: Class, otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>, retrySend: Boolean): UntrustworthyData { diff --git a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt b/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt deleted file mode 100644 index 38a239f949..0000000000 --- a/node/src/test/kotlin/net/corda/node/internal/NodeTest.kt +++ /dev/null @@ -1,20 +0,0 @@ -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.core.utilities.WHITESPACE -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 = baseDirectory(ALICE.name) - (baseDirectory / "plugins").createDirectories() - startNode(ALICE.name).getOrThrow() - } -} diff --git a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt index f0322fb400..bb6f59c78f 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/AttachmentTests.kt @@ -31,14 +31,14 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class AttachmentTests { - lateinit var network: MockNetwork + lateinit var mockNet: MockNetwork lateinit var dataSource: Closeable lateinit var database: Database lateinit var configuration: RequeryConfiguration @Before fun setUp() { - network = MockNetwork() + mockNet = MockNetwork() val dataSourceProperties = makeTestDataSourceProperties() @@ -57,7 +57,7 @@ class AttachmentTests { @Test fun `download and store`() { - val (n0, n1) = network.createTwoNodes() + val (n0, n1) = mockNet.createTwoNodes() // Insert an attachment into node zero's store directly. val id = n0.database.transaction { @@ -65,9 +65,9 @@ class AttachmentTests { } // Get node one to run a flow to fetch it and insert it. - network.runNetwork() + mockNet.runNetwork() val f1 = n1.services.startFlow(FetchAttachmentsFlow(setOf(id), n0.info.legalIdentity)) - network.runNetwork() + mockNet.runNetwork() assertEquals(0, f1.resultFuture.getOrThrow().fromDisk.size) // Verify it was inserted into node one's store. @@ -86,13 +86,13 @@ class AttachmentTests { @Test fun `missing`() { - val (n0, n1) = network.createTwoNodes() + val (n0, n1) = mockNet.createTwoNodes() // Get node one to fetch a non-existent attachment. val hash = SecureHash.randomSHA256() - network.runNetwork() + mockNet.runNetwork() val f1 = n1.services.startFlow(FetchAttachmentsFlow(setOf(hash), n0.info.legalIdentity)) - network.runNetwork() + mockNet.runNetwork() val e = assertFailsWith { f1.resultFuture.getOrThrow() } assertEquals(hash, e.requested) } @@ -100,7 +100,7 @@ class AttachmentTests { @Test fun `malicious response`() { // Make a node that doesn't do sanity checking at load time. - val n0 = network.createNode(null, -1, object : MockNetwork.Factory { + val n0 = mockNet.createNode(null, -1, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, @@ -114,7 +114,7 @@ class AttachmentTests { } } }, true, null, null, ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type)) - val n1 = network.createNode(n0.info.address) + val n1 = mockNet.createNode(n0.info.address) val attachment = fakeAttachment() // Insert an attachment into node zero's store directly. @@ -135,9 +135,9 @@ class AttachmentTests { // Get n1 to fetch the attachment. Should receive corrupted bytes. - network.runNetwork() + mockNet.runNetwork() val f1 = n1.services.startFlow(FetchAttachmentsFlow(setOf(id), n0.info.legalIdentity)) - network.runNetwork() + mockNet.runNetwork() assertFailsWith { f1.resultFuture.getOrThrow() } } } diff --git a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt index e62045e315..bf6c2755fe 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/InMemoryMessagingTests.kt @@ -14,7 +14,7 @@ import kotlin.test.assertFails import kotlin.test.assertTrue class InMemoryMessagingTests { - val network = MockNetwork() + val mockNet = MockNetwork() @Test fun topicStringValidation() { @@ -33,45 +33,45 @@ class InMemoryMessagingTests { @Test fun basics() { - val node1 = network.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = network.createNode(networkMapAddress = node1.info.address) - val node3 = network.createNode(networkMapAddress = node1.info.address) + val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node2 = mockNet.createNode(networkMapAddress = node1.info.address) + val node3 = mockNet.createNode(networkMapAddress = node1.info.address) val bits = "test-content".toByteArray() var finalDelivery: Message? = null with(node2) { - node2.net.addMessageHandler { msg, _ -> - node2.net.send(msg, node3.info.address) + node2.network.addMessageHandler { msg, _ -> + node2.network.send(msg, node3.info.address) } } with(node3) { - node2.net.addMessageHandler { msg, _ -> + node2.network.addMessageHandler { msg, _ -> finalDelivery = msg } } // Node 1 sends a message and it should end up in finalDelivery, after we run the network - node1.net.send(node1.net.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.info.address) + node1.network.send(node1.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), node2.info.address) - network.runNetwork(rounds = 1) + mockNet.runNetwork(rounds = 1) assertTrue(Arrays.equals(finalDelivery!!.data, bits)) } @Test fun broadcast() { - val node1 = network.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = network.createNode(networkMapAddress = node1.info.address) - val node3 = network.createNode(networkMapAddress = node1.info.address) + val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node2 = mockNet.createNode(networkMapAddress = node1.info.address) + val node3 = mockNet.createNode(networkMapAddress = node1.info.address) val bits = "test-content".toByteArray() var counter = 0 - listOf(node1, node2, node3).forEach { it.net.addMessageHandler { _, _ -> counter++ } } - node1.net.send(node2.net.createMessage("test.topic", DEFAULT_SESSION_ID, bits), network.messagingNetwork.everyoneOnline) - network.runNetwork(rounds = 1) + listOf(node1, node2, node3).forEach { it.network.addMessageHandler { _, _ -> counter++ } } + node1.network.send(node2.network.createMessage("test.topic", DEFAULT_SESSION_ID, bits), mockNet.messagingNetwork.everyoneOnline) + mockNet.runNetwork(rounds = 1) assertEquals(3, counter) } @@ -81,31 +81,31 @@ class InMemoryMessagingTests { */ @Test fun `skip unhandled messages`() { - val node1 = network.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) - val node2 = network.createNode(networkMapAddress = node1.info.address) + val node1 = mockNet.createNode(advertisedServices = ServiceInfo(NetworkMapService.type)) + val node2 = mockNet.createNode(networkMapAddress = node1.info.address) var received: Int = 0 - node1.net.addMessageHandler("valid_message") { _, _ -> + node1.network.addMessageHandler("valid_message") { _, _ -> received++ } - val invalidMessage = node2.net.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) - val validMessage = node2.net.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) - node2.net.send(invalidMessage, node1.net.myAddress) - network.runNetwork() + val invalidMessage = node2.network.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) + val validMessage = node2.network.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) + node2.network.send(invalidMessage, node1.network.myAddress) + mockNet.runNetwork() assertEquals(0, received) - node2.net.send(validMessage, node1.net.myAddress) - network.runNetwork() + node2.network.send(validMessage, node1.network.myAddress) + mockNet.runNetwork() assertEquals(1, received) // Here's the core of the test; previously the unhandled message would cause runNetwork() to abort early, so // this would fail. Make fresh messages to stop duplicate uniqueMessageId causing drops - val invalidMessage2 = node2.net.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) - val validMessage2 = node2.net.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) - node2.net.send(invalidMessage2, node1.net.myAddress) - node2.net.send(validMessage2, node1.net.myAddress) - network.runNetwork() + val invalidMessage2 = node2.network.createMessage("invalid_message", DEFAULT_SESSION_ID, ByteArray(0)) + val validMessage2 = node2.network.createMessage("valid_message", DEFAULT_SESSION_ID, ByteArray(0)) + node2.network.send(invalidMessage2, node1.network.myAddress) + node2.network.send(validMessage2, node1.network.myAddress) + mockNet.runNetwork() assertEquals(2, received) } } 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 1cde75adcf..6256e5ec3e 100644 --- a/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt +++ b/node/src/test/kotlin/net/corda/node/messaging/TwoPartyTradeFlowTests.kt @@ -4,24 +4,19 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.contracts.asset.* import net.corda.contracts.testing.fillWithSomeTestCash +import net.corda.core.* import net.corda.core.contracts.* 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.flows.* +import net.corda.core.identity.AbstractParty 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.internal.FlowStateMachine 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 @@ -65,11 +60,11 @@ import kotlin.test.assertTrue * We assume that Alice and Bob already found each other via some market, and have agreed the details already. */ class TwoPartyTradeFlowTests { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork @Before fun before() { - net = MockNetwork(false) + mockNet = MockNetwork(false) LogHelper.setLevel("platform.trade", "core.contract.TransactionGroup", "recordingmap") } @@ -83,12 +78,13 @@ class TwoPartyTradeFlowTests { // We run this in parallel threads to help catch any race conditions that may exist. The other tests // we run in the unit test thread exclusively to speed things up, ensure deterministic results and // allow interruption half way through. - net = MockNetwork(false, true) + mockNet = MockNetwork(false, true) ledger { - 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 basketOfNodes = mockNet.createSomeNodes(2) + val notaryNode = basketOfNodes.notaryNode + val aliceNode = basketOfNodes.partyNodes[0] + val bobNode = basketOfNodes.partyNodes[1] aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() @@ -127,18 +123,17 @@ class TwoPartyTradeFlowTests { @Test(expected = InsufficientBalanceException::class) fun `trade cash for commercial paper fails using soft locking`() { - net = MockNetwork(false, true) + mockNet = MockNetwork(false, true) ledger { - 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 notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() - val cashStates = - bobNode.database.transaction { + val cashStates = bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, notaryNode.info.notaryIdentity, 3, 3) } @@ -177,16 +172,16 @@ class TwoPartyTradeFlowTests { @Test fun `shutdown and restore`() { ledger { - val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) - val aliceNode = net.createPartyNode(notaryNode.info.address, ALICE.name) - var bobNode = net.createPartyNode(notaryNode.info.address, BOB.name) + val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) + var bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) aliceNode.disableDBCloseOnStop() bobNode.disableDBCloseOnStop() - val bobAddr = bobNode.net.myAddress as InMemoryMessagingNetwork.PeerHandle + val bobAddr = bobNode.network.myAddress as InMemoryMessagingNetwork.PeerHandle val networkMapAddr = notaryNode.info.address - net.runNetwork() // Clear network map registration messages + mockNet.runNetwork() // Clear network map registration messages bobNode.database.transaction { bobNode.services.fillWithSomeTestCash(2000.DOLLARS, outputNotary = notaryNode.info.notaryIdentity) @@ -230,7 +225,7 @@ class TwoPartyTradeFlowTests { // ... bring the node back up ... the act of constructing the SMM will re-register the message handlers // that Bob was waiting on before the reboot occurred. - bobNode = net.createNode(networkMapAddr, bobAddr.id, object : MockNetwork.Factory { + bobNode = mockNet.createNode(networkMapAddr, bobAddr.id, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { @@ -239,10 +234,10 @@ class TwoPartyTradeFlowTests { }, true, BOB.name) // Find the future representing the result of this state machine again. - val bobFuture = bobNode.smm.findStateMachines(Buyer::class.java).single().second + val bobFuture = bobNode.smm.findStateMachines(BuyerAcceptor::class.java).single().second // And off we go again. - net.runNetwork() + mockNet.runNetwork() // Bob is now finished and has the same transaction as Alice. assertThat(bobFuture.getOrThrow()).isEqualTo(aliceFuture.getOrThrow()) @@ -272,7 +267,7 @@ class TwoPartyTradeFlowTests { name: X500Name, overrideServices: Map? = null): MockNetwork.MockNode { // Create a node in the mock network ... - return net.createNode(networkMapAddr, -1, object : MockNetwork.Factory { + return mockNet.createNode(networkMapAddr, -1, object : MockNetwork.Factory { override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, @@ -295,7 +290,7 @@ class TwoPartyTradeFlowTests { @Test fun `check dependencies of sale asset are resolved`() { - val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) @@ -312,7 +307,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val extraKey = bobNode.keyManagement.freshKey() + val extraKey = bobNode.keyManagement.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(extraKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -323,11 +318,11 @@ class TwoPartyTradeFlowTests { } val alicesSignedTxns = insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) - net.runNetwork() // Clear network map registration messages + mockNet.runNetwork() // Clear network map registration messages runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()) - net.runNetwork() + mockNet.runNetwork() run { val records = (bobNode.storage.validatedTransactions as RecordingTransactionStorage).records @@ -394,7 +389,7 @@ class TwoPartyTradeFlowTests { @Test fun `track works`() { - val notaryNode = net.createNotaryNode(null, DUMMY_NOTARY.name) + val notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) val aliceNode = makeNodeWithTracking(notaryNode.info.address, ALICE.name) val bobNode = makeNodeWithTracking(notaryNode.info.address, BOB.name) @@ -411,7 +406,7 @@ class TwoPartyTradeFlowTests { attachment(ByteArrayInputStream(stream.toByteArray())) } - val bobsKey = bobNode.keyManagement.freshKey() + val bobsKey = bobNode.keyManagement.keys.single() val bobsFakeCash = fillUpForBuyer(false, AnonymousParty(bobsKey), DUMMY_CASH_ISSUER.party, notaryNode.info.notaryIdentity).second @@ -424,14 +419,14 @@ class TwoPartyTradeFlowTests { insertFakeTransactions(alicesFakePaper, aliceNode, notaryNode, MEGA_CORP_PUBKEY) - net.runNetwork() // Clear network map registration messages + mockNet.runNetwork() // Clear network map registration messages val aliceTxStream = aliceNode.storage.validatedTransactions.track().second val aliceTxMappings = with(aliceNode) { database.transaction { storage.stateMachineRecordedTransactionMapping.track().second } } val aliceSmId = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()).sellerId - net.runNetwork() + mockNet.runNetwork() // We need to declare this here, if we do it inside [expectEvents] kotlin throws an internal compiler error(!). val aliceTxExpectations = sequence( @@ -474,7 +469,7 @@ class TwoPartyTradeFlowTests { @Test fun `dependency with error on seller side`() { ledger { - runWithError(false, true, "must be timestamped") + runWithError(false, true, "Issuances must have a time-window") } } @@ -489,25 +484,42 @@ class TwoPartyTradeFlowTests { sellerNode: MockNetwork.MockNode, buyerNode: MockNetwork.MockNode, assetToSell: StateAndRef): RunResult { - @InitiatingFlow - class SellerRunnerFlow(val buyer: Party, val notary: NodeInfo) : FlowLogic() { - @Suspendable - override fun call(): SignedTransaction = subFlow(Seller( - buyer, - notary, - assetToSell, - 1000.DOLLARS, - serviceHub.legalIdentityKey)) - } + sellerNode.services.identityService.registerIdentity(buyerNode.info.legalIdentityAndCert) + buyerNode.services.identityService.registerIdentity(sellerNode.info.legalIdentityAndCert) + val buyerFlows: Observable = buyerNode.registerInitiatedFlow(BuyerAcceptor::class.java) + val firstBuyerFiber = buyerFlows.toFuture().map { it.stateMachine } + val seller = SellerInitiator(buyerNode.info.legalIdentity, notaryNode.info, assetToSell, 1000.DOLLARS) + val sellerResult = sellerNode.services.startFlow(seller).resultFuture + return RunResult(firstBuyerFiber, sellerResult, seller.stateMachine.id) + } - 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 } - val seller = SellerRunnerFlow(buyerNode.info.legalIdentity, notaryNode.info) - val sellerResultFuture = sellerNode.services.startFlow(seller).resultFuture - return RunResult(buyerFuture, sellerResultFuture, seller.stateMachine.id) + @InitiatingFlow + class SellerInitiator(val buyer: Party, + val notary: NodeInfo, + val assetToSell: StateAndRef, + val price: Amount) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + send(buyer, Pair(notary.notaryIdentity, price)) + return subFlow(Seller( + buyer, + notary, + assetToSell, + price, + serviceHub.legalIdentityKey)) + } + } + + @InitiatedBy(SellerInitiator::class) + class BuyerAcceptor(val seller: Party) : FlowLogic() { + @Suspendable + override fun call(): SignedTransaction { + val (notary, price) = receive>>(seller).unwrap { + require(serviceHub.networkMapCache.isNotary(it.first)) { "${it.first} is not a notary" } + it + } + return subFlow(Buyer(seller, notary, price, CommercialPaper.State::class.java)) + } } private fun LedgerDSL.runWithError( @@ -515,9 +527,9 @@ class TwoPartyTradeFlowTests { aliceError: Boolean, expectedMessageSubstring: String ) { - 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 notaryNode = mockNet.createNotaryNode(null, DUMMY_NOTARY.name) + val aliceNode = mockNet.createPartyNode(notaryNode.info.address, ALICE.name) + val bobNode = mockNet.createPartyNode(notaryNode.info.address, BOB.name) val issuer = MEGA_CORP.ref(1, 2, 3) val bobsBadCash = bobNode.database.transaction { @@ -532,11 +544,11 @@ class TwoPartyTradeFlowTests { 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 + mockNet.runNetwork() // Clear network map registration messages val (bobStateMachine, aliceResult) = runBuyerAndSeller(notaryNode, aliceNode, bobNode, "alice's paper".outputStateAndRef()) - net.runNetwork() + mockNet.runNetwork() val e = assertFailsWith { if (bobError) @@ -602,7 +614,7 @@ class TwoPartyTradeFlowTests { // Put a broken command on so at least a signature is created command(issuer.owningKey) { Cash.Commands.Move() } } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) if (withError) { this.fails() } else { @@ -642,7 +654,7 @@ class TwoPartyTradeFlowTests { } command(MEGA_CORP_PUBKEY) { CommercialPaper.Commands.Issue() } if (!withError) - timestamp(time = TEST_TX_TIME) + timeWindow(time = TEST_TX_TIME) if (attachmentID != null) attachment(attachmentID) if (withError) { 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 e79a83b653..061655f22b 100644 --- a/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt +++ b/node/src/test/kotlin/net/corda/node/services/MockServiceHubInternal.kt @@ -3,11 +3,11 @@ package net.corda.node.services import com.codahale.metrics.MetricRegistry import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.node.services.* +import net.corda.core.serialization.SerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.node.internal.ServiceFlowInfo +import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.serialization.NodeClock import net.corda.node.services.api.* import net.corda.node.services.messaging.MessagingService @@ -23,7 +23,7 @@ import java.time.Clock open class MockServiceHubInternal( val customVault: VaultService? = null, val keyManagement: KeyManagementService? = null, - val net: MessagingService? = null, + val network: MessagingService? = null, val identity: IdentityService? = MOCK_IDENTITY_SERVICE, val storage: TxWritableStorageService? = MockStorageService(), val mapCache: NetworkMapCacheInternal? = MockNetworkMapCache(), @@ -41,7 +41,7 @@ open class MockServiceHubInternal( override val identityService: IdentityService get() = identity ?: throw UnsupportedOperationException() override val networkService: MessagingService - get() = net ?: throw UnsupportedOperationException() + get() = network ?: throw UnsupportedOperationException() override val networkMapCache: NetworkMapCacheInternal get() = mapCache ?: throw UnsupportedOperationException() override val storageService: StorageService @@ -67,11 +67,11 @@ open class MockServiceHubInternal( override fun recordTransactions(txs: Iterable) = recordTransactionsInternal(txStorageService, txs) + override fun cordaService(type: Class): T = throw UnsupportedOperationException() + override fun startFlow(logic: FlowLogic, flowInitiator: FlowInitiator): FlowStateMachineImpl { return smm.executor.fetchFrom { smm.add(logic, flowInitiator) } } - override fun registerServiceFlow(initiatingFlowClass: Class>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit - - override fun getServiceFlowFactory(clientFlowClass: Class>): ServiceFlowInfo? = null + override fun getFlowFactory(initiatingFlowClass: Class>): InitiatedFlowFactory<*>? = null } 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 c0be61f475..84954fa218 100644 --- a/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/NotaryChangeTests.kt @@ -8,6 +8,7 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.seconds import net.corda.core.transactions.WireTransaction import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.flows.NotaryChangeFlow import net.corda.flows.StateReplacementException import net.corda.node.internal.AbstractNode @@ -24,7 +25,7 @@ import kotlin.test.assertEquals import kotlin.test.assertTrue class NotaryChangeTests { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var oldNotaryNode: MockNetwork.MockNode lateinit var newNotaryNode: MockNetwork.MockNode lateinit var clientNodeA: MockNetwork.MockNode @@ -32,15 +33,15 @@ class NotaryChangeTests { @Before fun setup() { - net = MockNetwork() - oldNotaryNode = net.createNode( + mockNet = MockNetwork() + oldNotaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNodeA = net.createNode(networkMapAddress = oldNotaryNode.info.address) - clientNodeB = net.createNode(networkMapAddress = oldNotaryNode.info.address) - newNotaryNode = net.createNode(networkMapAddress = oldNotaryNode.info.address, advertisedServices = ServiceInfo(SimpleNotaryService.type)) + clientNodeA = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) + clientNodeB = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address) + newNotaryNode = mockNet.createNode(networkMapAddress = oldNotaryNode.info.address, advertisedServices = ServiceInfo(SimpleNotaryService.type)) - net.runNetwork() // Clear network map registration messages + mockNet.runNetwork() // Clear network map registration messages } @Test @@ -50,7 +51,7 @@ class NotaryChangeTests { val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) - net.runNetwork() + mockNet.runNetwork() val newState = future.resultFuture.getOrThrow() assertEquals(newState.state.notary, newNotary) @@ -63,7 +64,7 @@ class NotaryChangeTests { val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) - net.runNetwork() + mockNet.runNetwork() val newState = future.resultFuture.getOrThrow() assertEquals(newState.state.notary, newNotary) @@ -75,11 +76,11 @@ class NotaryChangeTests { @Test fun `should throw when a participant refuses to change Notary`() { val state = issueMultiPartyState(clientNodeA, clientNodeB, oldNotaryNode) - val newEvilNotary = Party(X500Name("CN=Evil Notary,O=Evil R3,OU=corda,L=London,C=UK"), generateKeyPair().public) - val flow = NotaryChangeFlow(state, newEvilNotary) + val newEvilNotary = getTestPartyAndCertificate(X500Name("CN=Evil Notary,O=Evil R3,OU=corda,L=London,C=GB"), generateKeyPair().public) + val flow = NotaryChangeFlow(state, newEvilNotary.party) val future = clientNodeA.services.startFlow(flow) - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(StateReplacementException::class.java).isThrownBy { future.resultFuture.getOrThrow() @@ -94,7 +95,7 @@ class NotaryChangeTests { val newNotary = newNotaryNode.info.notaryIdentity val flow = NotaryChangeFlow(state, newNotary) val future = clientNodeA.services.startFlow(flow) - net.runNetwork() + mockNet.runNetwork() val newState = future.resultFuture.getOrThrow() assertEquals(newState.state.notary, newNotary) @@ -170,7 +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) + tx.addTimeWindow(Instant.now(), 30.seconds) 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/database/RequeryConfigurationTest.kt b/node/src/test/kotlin/net/corda/node/services/database/RequeryConfigurationTest.kt index 447769ea45..6d892f456c 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 @@ -167,7 +167,7 @@ class RequeryConfigurationTest { notary = DUMMY_NOTARY, signers = emptyList(), type = TransactionType.General, - timestamp = null + timeWindow = null ) return SignedTransaction(wtx.serialized, listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1)))) } 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 51c9f06c92..71754dd4d5 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 @@ -10,8 +10,10 @@ import net.corda.core.node.ServiceHub import net.corda.core.node.services.VaultService import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.utilities.ALICE_KEY +import net.corda.core.utilities.DUMMY_CA import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.services.MockServiceHubInternal +import net.corda.node.services.identity.InMemoryIdentityService import net.corda.node.services.persistence.DBCheckpointStorage import net.corda.node.services.statemachine.FlowLogicRefFactoryImpl import net.corda.node.services.statemachine.StateMachineManager @@ -75,22 +77,23 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { val dataSourceAndDatabase = configureDatabase(dataSourceProps) dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second + val identityService = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) + val kms = MockKeyManagementService(identityService, ALICE_KEY) 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) - services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, net = mockMessagingService), TestReference { + services = object : MockServiceHubInternal(overrideClock = testClock, keyManagement = kms, network = mockMessagingService), TestReference { override val vaultService: VaultService = NodeVaultService(this, dataSourceProps) override val testReference = this@NodeSchedulerServiceTest } scheduler = NodeSchedulerService(services, database, schedulerGatedExecutor) smmExecutor = AffinityExecutor.ServiceAffinityExecutor("test", 1) - val mockSMM = StateMachineManager(services, listOf(services, scheduler), DBCheckpointStorage(), smmExecutor, database) + val mockSMM = StateMachineManager(services, DBCheckpointStorage(), smmExecutor, database) mockSMM.changes.subscribe { change -> if (change is StateMachineManager.Change.Removed && mockSMM.allStateMachines.isEmpty()) { smmHasRemovedAllFlows.countDown() @@ -121,7 +124,9 @@ class NodeSchedulerServiceTest : SingletonSerializeAsToken() { override fun isRelevant(ourKeys: Set): Boolean = true - override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? = ScheduledActivity(flowLogicRef, instant) + override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { + return ScheduledActivity(flowLogicRef, instant) + } override val contract: Contract get() = throw UnsupportedOperationException() 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 db951ed371..10a31f2cbe 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 @@ -27,7 +27,7 @@ import java.time.Instant import kotlin.test.assertEquals class ScheduledFlowTests { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var nodeA: MockNetwork.MockNode lateinit var nodeB: MockNetwork.MockNode @@ -90,18 +90,18 @@ class ScheduledFlowTests { @Before fun setup() { - net = MockNetwork(threadPerNode = true) - notaryNode = net.createNode( + mockNet = MockNetwork(threadPerNode = true) + notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type))) - nodeA = net.createNode(notaryNode.info.address, start = false) - nodeB = net.createNode(notaryNode.info.address, start = false) - net.startNodes() + nodeA = mockNet.createNode(notaryNode.info.address, start = false) + nodeB = mockNet.createNode(notaryNode.info.address, start = false) + mockNet.startNodes() } @After fun cleanUp() { - net.stopNodes() + mockNet.stopNodes() } @Test @@ -116,7 +116,7 @@ class ScheduledFlowTests { } } nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) - net.waitQuiescent() + mockNet.waitQuiescent() val stateFromA = nodeA.database.transaction { nodeA.services.vaultService.linearHeadsOfType().values.first() } @@ -135,7 +135,7 @@ class ScheduledFlowTests { nodeA.services.startFlow(InsertInitialStateFlow(nodeB.info.legalIdentity)) nodeB.services.startFlow(InsertInitialStateFlow(nodeA.info.legalIdentity)) } - net.waitQuiescent() + mockNet.waitQuiescent() val statesFromA = nodeA.database.transaction { nodeA.services.vaultService.linearHeadsOfType() } 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 c47e9b3b7c..47a45d8e9b 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 @@ -39,7 +39,7 @@ import java.security.KeyPair import java.time.Instant abstract class AbstractNetworkMapServiceTest { - lateinit var network: MockNetwork + lateinit var mockNet: MockNetwork lateinit var mapServiceNode: MockNode lateinit var alice: MockNode @@ -49,18 +49,18 @@ abstract class AbstractNetworkMapServiceTest @Before fun setup() { - network = MockNetwork(defaultFactory = nodeFactory) - network.createTwoNodes(firstNodeName = DUMMY_MAP.name, secondNodeName = ALICE.name).apply { + mockNet = MockNetwork(defaultFactory = nodeFactory) + mockNet.createTwoNodes(firstNodeName = DUMMY_MAP.name, secondNodeName = ALICE.name).apply { mapServiceNode = first alice = second } - network.runNetwork() + mockNet.runNetwork() lastSerial = System.currentTimeMillis() } @After fun tearDown() { - network.stopNodes() + mockNet.stopNodes() } protected abstract val nodeFactory: MockNetwork.Factory @@ -188,7 +188,7 @@ abstract class AbstractNetworkMapServiceTest private fun MockNode.fetchMap(subscribe: Boolean = false, ifChangedSinceVersion: Int? = null): List { val request = FetchMapRequest(subscribe, ifChangedSinceVersion, info.address) val response = services.networkService.sendRequest(FETCH_TOPIC, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() return response.getOrThrow().nodes?.map { it.toChanged() } ?: emptyList() } @@ -198,9 +198,9 @@ abstract class AbstractNetworkMapServiceTest } private fun MockNode.identityQuery(): NodeInfo? { - val request = QueryIdentityRequest(info.legalIdentity, info.address) + val request = QueryIdentityRequest(info.legalIdentityAndCert, info.address) val response = services.networkService.sendRequest(QUERY_TOPIC, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() return response.getOrThrow().node } @@ -218,7 +218,7 @@ abstract class AbstractNetworkMapServiceTest val nodeRegistration = NodeRegistration(info, distinctSerial, addOrRemove, expires) val request = RegistrationRequest(nodeRegistration.toWire(services.keyManagementService, services.legalIdentityKey), info.address) val response = services.networkService.sendRequest(REGISTER_TOPIC, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() return response } @@ -229,7 +229,7 @@ abstract class AbstractNetworkMapServiceTest updates += message.data.deserialize() } val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() return updates } @@ -237,25 +237,25 @@ abstract class AbstractNetworkMapServiceTest private fun MockNode.unsubscribe() { val request = SubscribeRequest(false, info.address) val response = services.networkService.sendRequest(SUBSCRIPTION_TOPIC, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() assertThat(response.getOrThrow().confirmed).isTrue() } private fun MockNode.ackUpdate(mapVersion: Int) { val request = UpdateAcknowledge(mapVersion, services.networkService.myAddress) services.networkService.send(PUSH_ACK_TOPIC, DEFAULT_SESSION_ID, request, mapServiceNode.info.address) - network.runNetwork() + mockNet.runNetwork() } private fun addNewNodeToNetworkMap(legalName: X500Name): MockNode { - val node = network.createNode(networkMapAddress = mapServiceNode.info.address, legalName = legalName) - network.runNetwork() + val node = mockNet.createNode(networkMapAddress = mapServiceNode.info.address, legalName = legalName) + mockNet.runNetwork() lastSerial = System.currentTimeMillis() return node } private fun newNodeSeparateFromNetworkMap(legalName: X500Name): MockNode { - return network.createNode(legalName = legalName, nodeFactory = NoNMSNodeFactory) + return mockNet.createNode(legalName = legalName, nodeFactory = NoNMSNodeFactory) } sealed class Changed { 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 ce8c330e1d..8eed029284 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 @@ -3,14 +3,16 @@ package net.corda.node.services.network import net.corda.core.crypto.* import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate import net.corda.core.node.services.IdentityService -import net.corda.core.utilities.ALICE -import net.corda.core.utilities.BOB +import net.corda.core.utilities.* +import net.corda.flows.TxKeyFlow 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 java.security.cert.CertificateFactory import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertNull @@ -21,43 +23,58 @@ import kotlin.test.assertNull class InMemoryIdentityServiceTests { @Test fun `get all identities`() { - val service = InMemoryIdentityService() + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) + // Nothing registered, so empty set assertNull(service.getAllIdentities().firstOrNull()) - service.registerIdentity(ALICE) - var expected = setOf(ALICE) - var actual = service.getAllIdentities().toHashSet() + + service.registerIdentity(ALICE_IDENTITY) + var expected = setOf(ALICE) + var actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) // Add a second party and check we get both back - service.registerIdentity(BOB) - expected = setOf(ALICE, BOB) - actual = service.getAllIdentities().toHashSet() + service.registerIdentity(BOB_IDENTITY) + expected = setOf(ALICE, BOB) + actual = service.getAllIdentities().map { it.party }.toHashSet() assertEquals(expected, actual) } @Test fun `get identity by key`() { - val service = InMemoryIdentityService() + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) assertNull(service.partyFromKey(ALICE_PUBKEY)) - service.registerIdentity(ALICE) + service.registerIdentity(ALICE_IDENTITY) assertEquals(ALICE, service.partyFromKey(ALICE_PUBKEY)) assertNull(service.partyFromKey(BOB_PUBKEY)) } @Test fun `get identity by name with no registered identities`() { - val service = InMemoryIdentityService() + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) assertNull(service.partyFromX500Name(ALICE.name)) } + @Test + fun `get identity by substring match`() { + val trustRoot = DUMMY_CA + val service = InMemoryIdentityService(trustRoot = trustRoot.certificate) + service.registerIdentity(ALICE_IDENTITY) + service.registerIdentity(BOB_IDENTITY) + val alicente = getTestPartyAndCertificate(X500Name("O=Alicente Worldwide,L=London,C=GB"), generateKeyPair().public) + service.registerIdentity(alicente) + assertEquals(setOf(ALICE, alicente.party), service.partiesFromName("Alice", false)) + assertEquals(setOf(ALICE), service.partiesFromName("Alice Corp", true)) + assertEquals(setOf(BOB), service.partiesFromName("Bob Plc", true)) + } + @Test fun `get identity by name`() { - val service = InMemoryIdentityService() + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) val identities = listOf("Node A", "Node B", "Node C") - .map { Party(X500Name("CN=$it,O=R3,OU=corda,L=London,C=UK"), generateKeyPair().public) } + .map { getTestPartyAndCertificate(X500Name("CN=$it,O=R3,OU=corda,L=London,C=GB"), generateKeyPair().public) } assertNull(service.partyFromX500Name(identities.first().name)) identities.forEach { service.registerIdentity(it) } - identities.forEach { assertEquals(it, service.partyFromX500Name(it.name)) } + identities.forEach { assertEquals(it.party, service.partyFromX500Name(it.name)) } } /** @@ -68,7 +85,7 @@ class InMemoryIdentityServiceTests { val rootKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val rootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, rootKey) val txKey = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) - val service = InMemoryIdentityService() + val service = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate) // TODO: Generate certificate with an EdDSA key rather than ECDSA val identity = Party(CertificateAndKeyPair(rootCert, rootKey)) val txIdentity = AnonymousParty(txKey.public) @@ -84,35 +101,58 @@ class InMemoryIdentityServiceTests { */ @Test fun `assert ownership`() { - val aliceRootKey = Crypto.generateKeyPair() - val aliceRootCert = X509Utilities.createSelfSignedCACertificate(ALICE.name, aliceRootKey) - val aliceTxKey = Crypto.generateKeyPair() - val aliceTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, aliceRootCert, aliceRootKey, ALICE.name, aliceTxKey.public) - val aliceCertPath = X509Utilities.createCertificatePath(aliceRootCert, aliceTxCert, revocationEnabled = false) + val trustRoot = DUMMY_CA + val (alice, aliceTxIdentity) = createParty(ALICE.name, trustRoot) + val certFactory = CertificateFactory.getInstance("X509") val bobRootKey = Crypto.generateKeyPair() - val bobRootCert = X509Utilities.createSelfSignedCACertificate(BOB.name, bobRootKey) + val bobRoot = getTestPartyAndCertificate(BOB.name, bobRootKey.public) + val bobRootCert = bobRoot.certificate val bobTxKey = Crypto.generateKeyPair() - val bobTxCert = X509Utilities.createCertificate(CertificateType.INTERMEDIATE_CA, bobRootCert, bobRootKey, BOB.name, bobTxKey.public) - val bobCertPath = X509Utilities.createCertificatePath(bobRootCert, bobTxCert, revocationEnabled = false) + val bobTxCert = X509Utilities.createCertificate(CertificateType.IDENTITY, bobRootCert, bobRootKey, BOB.name, bobTxKey.public) + val bobCertPath = certFactory.generateCertPath(listOf(bobTxCert.cert, bobRootCert.cert)) + val bob = PartyAndCertificate(BOB.name, bobRootKey.public, bobRootCert, bobCertPath) + + // Now we have identities, construct the service and let it know about both + val service = InMemoryIdentityService(setOf(alice, bob), emptyMap(), trustRoot.certificate.cert) + service.registerAnonymousIdentity(aliceTxIdentity.identity, alice.party, aliceTxIdentity.certPath) - val service = InMemoryIdentityService() - val alice = Party(CertificateAndKeyPair(aliceRootCert, aliceRootKey)) - val anonymousAlice = AnonymousParty(aliceTxKey.public) - val bob = Party(CertificateAndKeyPair(bobRootCert, bobRootKey)) val anonymousBob = AnonymousParty(bobTxKey.public) - - service.registerPath(aliceRootCert, anonymousAlice, aliceCertPath) - service.registerPath(bobRootCert, anonymousBob, bobCertPath) + service.registerAnonymousIdentity(anonymousBob, bob.party, bobCertPath) // Verify that paths are verified - service.assertOwnership(alice, anonymousAlice) - service.assertOwnership(bob, anonymousBob) + service.assertOwnership(alice.party, aliceTxIdentity.identity) + service.assertOwnership(bob.party, anonymousBob) assertFailsWith { - service.assertOwnership(alice, anonymousBob) + service.assertOwnership(alice.party, anonymousBob) } assertFailsWith { - service.assertOwnership(bob, anonymousAlice) + service.assertOwnership(bob.party, aliceTxIdentity.identity) + } + + assertFailsWith { + val owningKey = Crypto.decodePublicKey(trustRoot.certificate.subjectPublicKeyInfo.encoded) + service.assertOwnership(Party(trustRoot.certificate.subject, owningKey), aliceTxIdentity.identity) } } + + private fun createParty(x500Name: X500Name, ca: CertificateAndKeyPair): Pair { + val certFactory = CertificateFactory.getInstance("X509") + val issuerKeyPair = generateKeyPair() + val issuer = getTestPartyAndCertificate(x500Name, issuerKeyPair.public, ca) + val txKey = Crypto.generateKeyPair() + val txCert = X509Utilities.createCertificate(CertificateType.IDENTITY, issuer.certificate, issuerKeyPair, x500Name, txKey.public) + val txCertPath = certFactory.generateCertPath(listOf(txCert.cert) + issuer.certPath.certificates) + return Pair(issuer, TxKeyFlow.AnonymousIdentity(txCertPath, txCert, AnonymousParty(txKey.public))) + } + + /** + * Ensure if we feed in a full identity, we get the same identity back. + */ + @Test + fun `deanonymising a well known identity`() { + val expected = ALICE + val actual = InMemoryIdentityService(trustRoot = DUMMY_CA.certificate).partyFromAnonymous(expected) + assertEquals(expected, actual) + } } diff --git a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt index cbcf699c36..a341420963 100644 --- a/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/network/InMemoryNetworkMapCacheTest.kt @@ -11,21 +11,21 @@ import java.math.BigInteger import kotlin.test.assertEquals class InMemoryNetworkMapCacheTest { - private val network = MockNetwork() + private val mockNet = MockNetwork() @Test fun registerWithNetwork() { - val (n0, n1) = network.createTwoNodes() - val future = n1.services.networkMapCache.addMapService(n1.net, n0.info.address, false, null) - network.runNetwork() + val (n0, n1) = mockNet.createTwoNodes() + val future = n1.services.networkMapCache.addMapService(n1.network, n0.info.address, false, null) + mockNet.runNetwork() future.getOrThrow() } @Test fun `key collision`() { val entropy = BigInteger.valueOf(24012017L) - val nodeA = network.createNode(null, -1, MockNetwork.DefaultFactory, true, ALICE.name, null, entropy, ServiceInfo(NetworkMapService.type)) - val nodeB = network.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type)) + val nodeA = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, ALICE.name, null, entropy, ServiceInfo(NetworkMapService.type)) + val nodeB = mockNet.createNode(null, -1, MockNetwork.DefaultFactory, true, BOB.name, null, entropy, ServiceInfo(NetworkMapService.type)) assertEquals(nodeA.info.legalIdentity, nodeB.info.legalIdentity) // Node A currently knows only about itself, so this returns node A diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt index 981a1ed894..24c2b17dba 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/DBTransactionStorageTests.kt @@ -154,7 +154,7 @@ class DBTransactionStorageTests { notary = DUMMY_NOTARY, signers = emptyList(), type = TransactionType.General, - timestamp = null + timeWindow = null ) return SignedTransaction(wtx.serialized, listOf(DigitalSignature.WithKey(NullPublicKey, ByteArray(1)))) } 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 8d969ce1c4..d5266caa8f 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 @@ -6,9 +6,10 @@ import net.corda.core.contracts.Amount import net.corda.core.contracts.Issued import net.corda.core.contracts.TransactionType import net.corda.core.contracts.USD -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party import net.corda.core.node.services.unconsumedStates import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY @@ -27,19 +28,19 @@ import kotlin.test.assertEquals * Tests for the data vending service. */ class DataVendingServiceTests { - lateinit var network: MockNetwork + lateinit var mockNet: MockNetwork @Before fun setup() { - network = MockNetwork() + mockNet = MockNetwork() } @Test fun `notify of transaction`() { - val (vaultServiceNode, registerNode) = network.createTwoNodes() + val (vaultServiceNode, registerNode) = mockNet.createTwoNodes() val beneficiary = vaultServiceNode.info.legalIdentity val deposit = registerNode.info.legalIdentity.ref(1) - network.runNetwork() + mockNet.runNetwork() // Generate an issuance transaction val ptx = TransactionType.General.Builder(null) @@ -64,10 +65,10 @@ class DataVendingServiceTests { */ @Test fun `notify failure`() { - val (vaultServiceNode, registerNode) = network.createTwoNodes() + val (vaultServiceNode, registerNode) = mockNet.createTwoNodes() val beneficiary = vaultServiceNode.info.legalIdentity val deposit = MEGA_CORP.ref(1) - network.runNetwork() + mockNet.runNetwork() // Generate an issuance transaction val ptx = TransactionType.General.Builder(DUMMY_NOTARY) @@ -86,16 +87,20 @@ class DataVendingServiceTests { } private fun MockNode.sendNotifyTx(tx: SignedTransaction, walletServiceNode: MockNode) { - walletServiceNode.registerServiceFlow(clientFlowClass = NotifyTxFlow::class, serviceFlowFactory = ::NotifyTransactionHandler) + walletServiceNode.registerInitiatedFlow(InitiateNotifyTxFlow::class.java) services.startFlow(NotifyTxFlow(walletServiceNode.info.legalIdentity, tx)) - network.runNetwork() + mockNet.runNetwork() } - @InitiatingFlow private class NotifyTxFlow(val otherParty: Party, val stx: SignedTransaction) : FlowLogic() { @Suspendable override fun call() = send(otherParty, NotifyTxRequest(stx)) } + @InitiatedBy(NotifyTxFlow::class) + private class InitiateNotifyTxFlow(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call() = subFlow(NotifyTransactionHandler(otherParty)) + } } diff --git a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt index d951f9d35b..1233f22951 100644 --- a/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt +++ b/node/src/test/kotlin/net/corda/node/services/persistence/NodeAttachmentStorageTest.kt @@ -14,9 +14,9 @@ import net.corda.node.services.database.RequeryConfiguration import net.corda.node.services.persistence.schemas.AttachmentEntity import net.corda.node.services.transactions.PersistentUniquenessProvider import net.corda.node.utilities.configureDatabase +import net.corda.node.utilities.transaction import net.corda.testing.node.makeTestDataSourceProperties import org.jetbrains.exposed.sql.Database -import org.jetbrains.exposed.sql.transactions.TransactionManager import org.junit.After import org.junit.Before import org.junit.Test @@ -55,7 +55,7 @@ class NodeAttachmentStorageTest { @After fun tearDown() { - TransactionManager.current().close() + dataSource.close() } @Test @@ -63,76 +63,84 @@ class NodeAttachmentStorageTest { val testJar = makeTestJar() val expectedHash = testJar.readAll().sha256() - val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) - val id = testJar.read { storage.importAttachment(it) } - assertEquals(expectedHash, id) + database.transaction { + val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) + val id = testJar.read { storage.importAttachment(it) } + assertEquals(expectedHash, id) - assertNull(storage.openAttachment(SecureHash.randomSHA256())) - val stream = storage.openAttachment(expectedHash)!!.openAsJAR() - val e1 = stream.nextJarEntry!! - assertEquals("test1.txt", e1.name) - assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "This is some useful content") - val e2 = stream.nextJarEntry!! - assertEquals("test2.txt", e2.name) - assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "Some more useful content") + assertNull(storage.openAttachment(SecureHash.randomSHA256())) + val stream = storage.openAttachment(expectedHash)!!.openAsJAR() + val e1 = stream.nextJarEntry!! + assertEquals("test1.txt", e1.name) + assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "This is some useful content") + val e2 = stream.nextJarEntry!! + assertEquals("test2.txt", e2.name) + assertEquals(stream.readBytes().toString(Charset.defaultCharset()), "Some more useful content") - stream.close() + stream.close() - storage.openAttachment(id)!!.openAsJAR().use { - it.nextJarEntry - it.readBytes() + storage.openAttachment(id)!!.openAsJAR().use { + it.nextJarEntry + it.readBytes() + } } } @Test fun `duplicates not allowed`() { val testJar = makeTestJar() - val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) - testJar.read { - storage.importAttachment(it) - } - assertFailsWith { + database.transaction { + val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) testJar.read { storage.importAttachment(it) } + assertFailsWith { + testJar.read { + storage.importAttachment(it) + } + } } } @Test fun `corrupt entry throws exception`() { val testJar = makeTestJar() - val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) - val id = testJar.read { storage.importAttachment(it) } + database.transaction { + val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) + val id = testJar.read { storage.importAttachment(it) } - // Corrupt the file in the store. - val bytes = testJar.readAll() - val corruptBytes = "arggghhhh".toByteArray() - System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) - val corruptAttachment = AttachmentEntity() - corruptAttachment.attId = id - corruptAttachment.content = bytes - storage.session.update(corruptAttachment) + // Corrupt the file in the store. + val bytes = testJar.readAll() + val corruptBytes = "arggghhhh".toByteArray() + System.arraycopy(corruptBytes, 0, bytes, 0, corruptBytes.size) + val corruptAttachment = AttachmentEntity() + corruptAttachment.attId = id + corruptAttachment.content = bytes + storage.session.update(corruptAttachment) - val e = assertFailsWith { - storage.openAttachment(id)!!.open().use { it.readBytes() } - } - assertEquals(e.expected, id) + val e = assertFailsWith { + storage.openAttachment(id)!!.open().use { it.readBytes() } + } + assertEquals(e.expected, id) - // But if we skip around and read a single entry, no exception is thrown. - storage.openAttachment(id)!!.openAsJAR().use { - it.nextJarEntry - it.readBytes() + // But if we skip around and read a single entry, no exception is thrown. + storage.openAttachment(id)!!.openAsJAR().use { + it.nextJarEntry + it.readBytes() + } } } @Test fun `non jar rejected`() { - val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) - val path = fs.getPath("notajar") - path.writeLines(listOf("Hey", "there!")) - path.read { - assertFailsWith("either empty or not a JAR") { - storage.importAttachment(it) + database.transaction { + val storage = NodeAttachmentService(fs.getPath("/"), dataSourceProperties, MetricRegistry()) + val path = fs.getPath("notajar") + path.writeLines(listOf("Hey", "there!")) + path.read { + assertFailsWith("either empty or not a JAR") { + storage.importAttachment(it) + } } } } 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 7eece7a705..54412d9e67 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 @@ -7,13 +7,13 @@ import net.corda.contracts.asset.Cash import net.corda.core.* import net.corda.core.contracts.DOLLARS import net.corda.core.contracts.DummyState -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash import net.corda.core.crypto.generateKeyPair import net.corda.core.flows.FlowException import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowSessionException import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party import net.corda.core.messaging.MessageRecipients import net.corda.core.node.services.PartyInfo import net.corda.core.node.services.ServiceInfo @@ -30,15 +30,19 @@ import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.flows.FinalityFlow import net.corda.flows.NotaryFlow +import net.corda.node.internal.InitiatedFlowFactory import net.corda.node.services.persistence.checkpoints import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.utilities.transaction -import net.corda.testing.* +import net.corda.testing.expect +import net.corda.testing.expectEvents +import net.corda.testing.getTestX509Name import net.corda.testing.node.InMemoryMessagingNetwork import net.corda.testing.node.InMemoryMessagingNetwork.MessageTransfer import net.corda.testing.node.InMemoryMessagingNetwork.ServicePeerAllocationStrategy.RoundRobin import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork.MockNode +import net.corda.testing.sequence import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.AssertionsForClassTypes.assertThatExceptionOfType @@ -60,7 +64,7 @@ class FlowFrameworkTests { } } - private val net = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) + private val mockNet = MockNetwork(servicePeerAllocationStrategy = RoundRobin()) private val sessionTransfers = ArrayList() private lateinit var node1: MockNode private lateinit var node2: MockNode @@ -69,7 +73,7 @@ class FlowFrameworkTests { @Before fun start() { - val nodes = net.createTwoNodes() + val nodes = mockNet.createTwoNodes() node1 = nodes.first node2 = nodes.second val notaryKeyPair = generateKeyPair() @@ -77,16 +81,16 @@ class FlowFrameworkTests { val overrideServices = mapOf(Pair(notaryService, notaryKeyPair)) // Note that these notaries don't operate correctly as they don't share their state. They are only used for testing // service addressing. - notary1 = net.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) - notary2 = net.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) + notary1 = mockNet.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) + notary2 = mockNet.createNotaryNode(networkMapAddr = node1.services.myInfo.address, overrideServices = overrideServices, serviceName = notaryService.name) - net.messagingNetwork.receivedMessages.toSessionTransfers().forEach { sessionTransfers += it } - net.runNetwork() + mockNet.messagingNetwork.receivedMessages.toSessionTransfers().forEach { sessionTransfers += it } + mockNet.runNetwork() } @After fun cleanUp() { - net.stopNodes() + mockNet.stopNodes() } @Test @@ -110,7 +114,7 @@ class FlowFrameworkTests { @Test fun `exception while fiber suspended`() { - node2.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) } + node2.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) } val flow = ReceiveFlow(node2.info.legalIdentity) val fiber = node1.services.startFlow(flow) as FlowStateMachineImpl // Before the flow runs change the suspend action to throw an exception @@ -118,7 +122,7 @@ class FlowFrameworkTests { fiber.actionOnSuspend = { throw exceptionDuringSuspend } - net.runNetwork() + mockNet.runNetwork() assertThatThrownBy { fiber.resultFuture.getOrThrow() }.isSameAs(exceptionDuringSuspend) @@ -129,7 +133,7 @@ class FlowFrameworkTests { @Test fun `flow restarted just after receiving payload`() { - node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } + node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } node1.services.startFlow(SendFlow("Hello", node2.info.legalIdentity)) // We push through just enough messages to get only the payload sent @@ -137,49 +141,49 @@ class FlowFrameworkTests { node2.disableDBCloseOnStop() node2.acceptableLiveFiberCountOnStop = 1 node2.stop() - net.runNetwork() + mockNet.runNetwork() val restoredFlow = node2.restartAndGetRestoredFlow(node1) assertThat(restoredFlow.receivedPayloads[0]).isEqualTo("Hello") } @Test fun `flow added before network map does run after init`() { - val node3 = net.createNode(node1.info.address) //create vanilla node + val node3 = mockNet.createNode(node1.info.address) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet - net.runNetwork() // Allow network map messages to flow + mockNet.runNetwork() // Allow network map messages to flow assertEquals(true, flow.flowStarted) // Now we should have run the flow } @Test fun `flow added before network map will be init checkpointed`() { - var node3 = net.createNode(node1.info.address) //create vanilla node + var node3 = mockNet.createNode(node1.info.address) //create vanilla node val flow = NoOpFlow() node3.services.startFlow(flow) assertEquals(false, flow.flowStarted) // Not started yet as no network activity has been allowed yet node3.disableDBCloseOnStop() node3.stop() - node3 = net.createNode(node1.info.address, forcedID = node3.id) + node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) val restoredFlow = node3.getSingleFlow().first assertEquals(false, restoredFlow.flowStarted) // Not started yet as no network activity has been allowed yet - net.runNetwork() // Allow network map messages to flow + mockNet.runNetwork() // Allow network map messages to flow node3.smm.executor.flush() assertEquals(true, restoredFlow.flowStarted) // Now we should have run the flow and hopefully cleared the init checkpoint node3.disableDBCloseOnStop() node3.stop() // Now it is completed the flow should leave no Checkpoint. - node3 = net.createNode(node1.info.address, forcedID = node3.id) - net.runNetwork() // Allow network map messages to flow + node3 = mockNet.createNode(node1.info.address, forcedID = node3.id) + mockNet.runNetwork() // Allow network map messages to flow node3.smm.executor.flush() assertTrue(node3.smm.findStateMachines(NoOpFlow::class.java).isEmpty()) } @Test fun `flow loaded from checkpoint will respond to messages from before start`() { - node1.registerServiceFlow(ReceiveFlow::class) { SendFlow("Hello", it) } + node1.registerFlowFactory(ReceiveFlow::class) { SendFlow("Hello", it) } node2.services.startFlow(ReceiveFlow(node1.info.legalIdentity).nonTerminating()) // Prepare checkpointed receive flow // Make sure the add() has finished initial processing. node2.smm.executor.flush() @@ -195,11 +199,11 @@ class FlowFrameworkTests { val payload2 = random63BitValue() var sentCount = 0 - net.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } + mockNet.messagingNetwork.sentMessages.toSessionTransfers().filter { it.isPayloadTransfer }.forEach { sentCount++ } - val node3 = net.createNode(node1.info.address) - val secondFlow = node3.initiateSingleShotFlow(PingPongFlow::class) { PingPongFlow(it, payload2) } - net.runNetwork() + val node3 = mockNet.createNode(node1.info.address) + val secondFlow = node3.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, payload2) } + mockNet.runNetwork() // Kick off first send and receive node2.services.startFlow(PingPongFlow(node3.info.legalIdentity, payload)) @@ -214,11 +218,11 @@ class FlowFrameworkTests { node2.database.transaction { assertEquals(1, node2.checkpointStorage.checkpoints().size) // confirm checkpoint } - val node2b = net.createNode(node1.info.address, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) + val node2b = mockNet.createNode(node1.info.address, node2.id, advertisedServices = *node2.advertisedServices.toTypedArray()) node2.manuallyCloseDB() val (firstAgain, fut1) = node2b.getSingleFlow() // Run the network which will also fire up the second flow. First message should get deduped. So message data stays in sync. - net.runNetwork() + mockNet.runNetwork() node2b.smm.executor.flush() fut1.getOrThrow() @@ -241,13 +245,13 @@ class FlowFrameworkTests { @Test fun `sending to multiple parties`() { - val node3 = net.createNode(node1.info.address) - net.runNetwork() - node2.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } - node3.registerServiceFlow(SendFlow::class) { ReceiveFlow(it).nonTerminating() } + val node3 = mockNet.createNode(node1.info.address) + mockNet.runNetwork() + node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } + node3.registerFlowFactory(SendFlow::class) { ReceiveFlow(it).nonTerminating() } val payload = "Hello World" node1.services.startFlow(SendFlow(payload, node2.info.legalIdentity, node3.info.legalIdentity)) - net.runNetwork() + mockNet.runNetwork() val node2Flow = node2.getSingleFlow().first val node3Flow = node3.getSingleFlow().first assertThat(node2Flow.receivedPayloads[0]).isEqualTo(payload) @@ -273,16 +277,16 @@ class FlowFrameworkTests { @Test fun `receiving from multiple parties`() { - val node3 = net.createNode(node1.info.address) - net.runNetwork() + val node3 = mockNet.createNode(node1.info.address) + mockNet.runNetwork() val node2Payload = "Test 1" val node3Payload = "Test 2" - node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(node2Payload, it) } - node3.registerServiceFlow(ReceiveFlow::class) { SendFlow(node3Payload, it) } + node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(node2Payload, it) } + node3.registerFlowFactory(ReceiveFlow::class) { SendFlow(node3Payload, it) } val multiReceiveFlow = ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity).nonTerminating() node1.services.startFlow(multiReceiveFlow) node1.acceptableLiveFiberCountOnStop = 1 - net.runNetwork() + mockNet.runNetwork() assertThat(multiReceiveFlow.receivedPayloads[0]).isEqualTo(node2Payload) assertThat(multiReceiveFlow.receivedPayloads[1]).isEqualTo(node3Payload) @@ -303,9 +307,9 @@ class FlowFrameworkTests { @Test fun `both sides do a send as their first IO request`() { - node2.registerServiceFlow(PingPongFlow::class) { PingPongFlow(it, 20L) } + node2.registerFlowFactory(PingPongFlow::class) { PingPongFlow(it, 20L) } node1.services.startFlow(PingPongFlow(node2.info.legalIdentity, 10L)) - net.runNetwork() + mockNet.runNetwork() assertSessionTransfers( node1 sent sessionInit(PingPongFlow::class, 1, 10L) to node2, @@ -328,9 +332,9 @@ class FlowFrameworkTests { // We pay a couple of times, the notary picking should go round robin for (i in 1..3) { node1.services.startFlow(CashPaymentFlow(500.DOLLARS, node2.info.legalIdentity)) - net.runNetwork() + mockNet.runNetwork() } - val endpoint = net.messagingNetwork.endpoint(notary1.net.myAddress as InMemoryMessagingNetwork.PeerHandle)!! + val endpoint = mockNet.messagingNetwork.endpoint(notary1.network.myAddress as InMemoryMessagingNetwork.PeerHandle)!! val party1Info = notary1.services.networkMapCache.getPartyInfo(notary1.info.notaryIdentity)!! assertTrue(party1Info is PartyInfo.Service) val notary1Address: MessageRecipients = endpoint.getAddressOfParty(notary1.services.networkMapCache.getPartyInfo(notary1.info.notaryIdentity)!!) @@ -339,7 +343,7 @@ class FlowFrameworkTests { sessionTransfers.expectEvents(isStrict = false) { sequence( // First Pay - expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { + expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) { it.message as SessionInit assertEquals(node1.id, it.from) assertEquals(notary1Address, it.to) @@ -349,7 +353,7 @@ class FlowFrameworkTests { assertEquals(notary1.id, it.from) }, // Second pay - expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { + expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) { it.message as SessionInit assertEquals(node1.id, it.from) assertEquals(notary1Address, it.to) @@ -359,7 +363,7 @@ class FlowFrameworkTests { assertEquals(notary2.id, it.from) }, // Third pay - expect(match = { it.message is SessionInit && it.message.clientFlowClass == NotaryFlow.Client::class.java }) { + expect(match = { it.message is SessionInit && it.message.initiatingFlowClass == NotaryFlow.Client::class.java }) { it.message as SessionInit assertEquals(node1.id, it.from) assertEquals(notary1Address, it.to) @@ -374,9 +378,9 @@ class FlowFrameworkTests { @Test fun `other side ends before doing expected send`() { - node2.registerServiceFlow(ReceiveFlow::class) { NoOpFlow() } + node2.registerFlowFactory(ReceiveFlow::class) { NoOpFlow() } val resultFuture = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { resultFuture.getOrThrow() }.withMessageContaining(String::class.java.name) // Make sure the exception message mentions the type the flow was expecting to receive @@ -384,7 +388,7 @@ class FlowFrameworkTests { @Test fun `non-FlowException thrown on other side`() { - val erroringFlowFuture = node2.initiateSingleShotFlow(ReceiveFlow::class) { + val erroringFlowFuture = node2.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { Exception("evil bug!") } } val erroringFlowSteps = erroringFlowFuture.flatMap { it.progressSteps } @@ -393,7 +397,7 @@ class FlowFrameworkTests { val receiveFlowSteps = receiveFlow.progressSteps val receiveFlowResult = node1.services.startFlow(receiveFlow).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(erroringFlowSteps.get()).containsExactly( Notification.createOnNext(ExceptionFlow.START_STEP), @@ -418,14 +422,14 @@ class FlowFrameworkTests { @Test fun `FlowException thrown on other side`() { - val erroringFlow = node2.initiateSingleShotFlow(ReceiveFlow::class) { + val erroringFlow = node2.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val erroringFlowSteps = erroringFlow.flatMap { it.progressSteps } val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) as FlowStateMachineImpl - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(MyFlowException::class.java) .isThrownBy { receivingFiber.resultFuture.getOrThrow() } @@ -453,13 +457,13 @@ class FlowFrameworkTests { @Test fun `FlowException propagated in invocation chain`() { - val node3 = net.createNode(node1.info.address) - net.runNetwork() + val node3 = mockNet.createNode(node1.info.address) + mockNet.runNetwork() - node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } - node2.initiateSingleShotFlow(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) } + node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Chain") } } + node2.registerFlowFactory(ReceiveFlow::class) { ReceiveFlow(node3.info.legalIdentity) } val receivingFiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)) - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(MyFlowException::class.java) .isThrownBy { receivingFiber.resultFuture.getOrThrow() } .withMessage("Chain") @@ -467,18 +471,18 @@ class FlowFrameworkTests { @Test fun `FlowException thrown and there is a 3rd unrelated party flow`() { - val node3 = net.createNode(node1.info.address) - net.runNetwork() + val node3 = mockNet.createNode(node1.info.address) + mockNet.runNetwork() // Node 2 will send its payload and then block waiting for the receive from node 1. Meanwhile node 1 will move // onto node 3 which will throw the exception val node2Fiber = node2 - .initiateSingleShotFlow(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } + .registerFlowFactory(ReceiveFlow::class) { SendAndReceiveFlow(it, "Hello") } .map { it.stateMachine } - node3.initiateSingleShotFlow(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } + node3.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { MyFlowException("Nothing useful") } } val node1Fiber = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity, node3.info.legalIdentity)) as FlowStateMachineImpl - net.runNetwork() + mockNet.runNetwork() // Node 1 will terminate with the error it received from node 3 but it won't propagate that to node 2 (as it's // not relevant to it) but it will end its session with it @@ -528,17 +532,17 @@ class FlowFrameworkTests { } } - node2.registerServiceFlow(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } + node2.registerFlowFactory(AskForExceptionFlow::class) { ConditionalExceptionFlow(it, "Hello") } val resultFuture = node1.services.startFlow(RetryOnExceptionFlow(node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(resultFuture.getOrThrow()).isEqualTo("Hello") } @Test fun `serialisation issue in counterparty`() { - node2.registerServiceFlow(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) } + node2.registerFlowFactory(ReceiveFlow::class) { SendFlow(NonSerialisableData(1), it) } val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { result.getOrThrow() } @@ -546,11 +550,11 @@ class FlowFrameworkTests { @Test fun `FlowException has non-serialisable object`() { - node2.initiateSingleShotFlow(ReceiveFlow::class) { + node2.registerFlowFactory(ReceiveFlow::class) { ExceptionFlow { NonSerialisableFlowException(NonSerialisableData(1)) } } val result = node1.services.startFlow(ReceiveFlow(node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(FlowException::class.java).isThrownBy { result.getOrThrow() } @@ -562,11 +566,11 @@ class FlowFrameworkTests { ptx.addOutputState(DummyState()) val stx = node1.services.signInitialTransaction(ptx) - val committerFiber = node1 - .initiateSingleShotFlow(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) } - .map { it.stateMachine } + val committerFiber = node1.registerFlowFactory(WaitingFlows.Waiter::class) { + WaitingFlows.Committer(it) + }.map { it.stateMachine } val waiterStx = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(waiterStx.getOrThrow()).isEqualTo(committerFiber.getOrThrow().resultFuture.getOrThrow()) } @@ -576,11 +580,11 @@ class FlowFrameworkTests { ptx.addOutputState(DummyState()) val stx = node1.services.signInitialTransaction(ptx) - node1.registerServiceFlow(WaitingFlows.Waiter::class) { + node1.registerFlowFactory(WaitingFlows.Waiter::class) { WaitingFlows.Committer(it) { throw Exception("Error") } } val waiter = node2.services.startFlow(WaitingFlows.Waiter(stx, node1.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { waiter.getOrThrow() } @@ -589,22 +593,31 @@ class FlowFrameworkTests { @Test fun `lazy db iterator left on stack during checkpointing`() { val result = node2.services.startFlow(VaultAccessFlow()).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatThrownBy { result.getOrThrow() }.hasMessageContaining("Vault").hasMessageContaining("private method") } @Test - fun `custom client flow`() { - val receiveFlowFuture = node2.initiateSingleShotFlow(SendFlow::class) { ReceiveFlow(it) } + fun `customised client flow`() { + val receiveFlowFuture = node2.registerFlowFactory(SendFlow::class) { ReceiveFlow(it) } node1.services.startFlow(CustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(receiveFlowFuture.getOrThrow().receivedPayloads).containsOnly("Hello") } + @Test + fun `customised client flow which has annotated @InitiatingFlow again`() { + val result = node1.services.startFlow(IncorrectCustomSendFlow("Hello", node2.info.legalIdentity)).resultFuture + mockNet.runNetwork() + assertThatExceptionOfType(IllegalArgumentException::class.java).isThrownBy { + result.getOrThrow() + }.withMessageContaining(InitiatingFlow::class.java.simpleName) + } + @Test fun `upgraded flow`() { node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)) - net.runNetwork() + mockNet.runNetwork() assertThat(sessionTransfers).startsWith( node1 sent sessionInit(UpgradedFlow::class, 2) to node2 ) @@ -612,9 +625,13 @@ class FlowFrameworkTests { @Test fun `unsupported new flow version`() { - node2.registerServiceFlow(UpgradedFlow::class, flowVersion = 1) { SendFlow("Hello", it) } + node2.internalRegisterFlowFactory( + UpgradedFlow::class.java, + InitiatedFlowFactory.CorDapp(version = 1, factory = ::DoubleInlinedSubFlow), + DoubleInlinedSubFlow::class.java, + track = false) val result = node1.services.startFlow(UpgradedFlow(node2.info.legalIdentity)).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThatExceptionOfType(FlowSessionException::class.java).isThrownBy { result.getOrThrow() }.withMessageContaining("Version") @@ -622,17 +639,17 @@ class FlowFrameworkTests { @Test fun `single inlined sub-flow`() { - node2.registerServiceFlow(SendAndReceiveFlow::class) { SingleInlinedSubFlow(it) } + node2.registerFlowFactory(SendAndReceiveFlow::class, ::SingleInlinedSubFlow) val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @Test fun `double inlined sub-flow`() { - node2.registerServiceFlow(SendAndReceiveFlow::class) { DoubleInlinedSubFlow(it) } + node2.registerFlowFactory(SendAndReceiveFlow::class, ::DoubleInlinedSubFlow) val result = node1.services.startFlow(SendAndReceiveFlow(node2.info.legalIdentity, "Hello")).resultFuture - net.runNetwork() + mockNet.runNetwork() assertThat(result.getOrThrow()).isEqualTo("HelloHello") } @@ -654,6 +671,18 @@ class FlowFrameworkTests { return smm.findStateMachines(P::class.java).single() } + private inline fun > MockNode.registerFlowFactory( + initiatingFlowClass: KClass>, + noinline flowFactory: (Party) -> P): ListenableFuture

+ { + val observable = internalRegisterFlowFactory(initiatingFlowClass.java, object : InitiatedFlowFactory

{ + override fun createFlow(platformVersion: Int, otherParty: Party, sessionInit: SessionInit): P { + return flowFactory(otherParty) + } + }, P::class.java, track = true) + return observable.toFuture() + } + private fun sessionInit(clientFlowClass: KClass>, flowVersion: Int = 1, payload: Any? = null): SessionInit { return SessionInit(0, clientFlowClass.java, flowVersion, payload) } @@ -667,7 +696,7 @@ class FlowFrameworkTests { } private fun assertSessionTransfers(node: MockNode, vararg expected: SessionTransfer): List { - val actualForNode = sessionTransfers.filter { it.from == node.id || it.to == node.net.myAddress } + val actualForNode = sessionTransfers.filter { it.from == node.id || it.to == node.network.myAddress } assertThat(actualForNode).containsExactly(*expected) return actualForNode } @@ -695,7 +724,7 @@ class FlowFrameworkTests { } private infix fun MockNode.sent(message: SessionMessage): Pair = Pair(id, message) - private infix fun Pair.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.net.myAddress) + private infix fun Pair.to(node: MockNode): SessionTransfer = SessionTransfer(first, second, node.network.myAddress) private val FlowLogic<*>.progressSteps: ListenableFuture>> get() { return progressTracker!!.changes @@ -730,8 +759,12 @@ class FlowFrameworkTests { } private interface CustomInterface + private class CustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) + @InitiatingFlow + private class IncorrectCustomSendFlow(payload: String, otherParty: Party) : CustomInterface, SendFlow(payload, otherParty) + @InitiatingFlow private class ReceiveFlow(vararg val otherParties: Party) : FlowLogic() { object START_STEP : ProgressTracker.Step("Starting") @@ -852,7 +885,7 @@ class FlowFrameworkTests { } private data class NonSerialisableData(val a: Int) - private class NonSerialisableFlowException(val data: NonSerialisableData) : FlowException() + private class NonSerialisableFlowException(@Suppress("unused") val data: NonSerialisableData) : FlowException() //endregion Helpers } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt index c18fe75d9e..0dd0af75dc 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/BFTSMaRtConfigTests.kt @@ -2,9 +2,9 @@ package net.corda.node.services.transactions import com.google.common.net.HostAndPort import net.corda.node.services.transactions.BFTSMaRtConfig.Companion.portIsClaimedFormat +import org.assertj.core.api.Assertions.assertThatThrownBy import org.junit.Test import kotlin.test.assertEquals -import kotlin.test.assertFailsWith class BFTSMaRtConfigTests { @Test @@ -29,12 +29,12 @@ class BFTSMaRtConfigTests { @Test fun `overlapping port ranges are rejected`() { fun addresses(vararg ports: Int) = ports.map { HostAndPort.fromParts("localhost", it) } - assertFailsWith(IllegalArgumentException::class, portIsClaimedFormat.format(11001, setOf(11000, 11001))) { - BFTSMaRtConfig(addresses(11000, 11001)).use {} - } - assertFailsWith(IllegalArgumentException::class, portIsClaimedFormat.format(11001, setOf(11001, 11002))) { - BFTSMaRtConfig(addresses(11001, 11000)).use {} - } + assertThatThrownBy { BFTSMaRtConfig(addresses(11000, 11001)).use {} } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11000", "localhost:11001"))) + assertThatThrownBy { BFTSMaRtConfig(addresses(11001, 11000)).use {} } + .isInstanceOf(IllegalArgumentException::class.java) + .hasMessage(portIsClaimedFormat.format("localhost:11001", setOf("localhost:11001", "localhost:11002", "localhost:11000"))) BFTSMaRtConfig(addresses(11000, 11002)).use {} // Non-overlapping. } } 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 408cd5a545..039ca933b6 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 @@ -26,24 +26,24 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class NotaryServiceTests { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode @Before fun setup() { - net = MockNetwork() - notaryNode = net.createNode( + mockNet = MockNetwork() + notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(SimpleNotaryService.type))) - clientNode = net.createNode(networkMapAddress = notaryNode.info.address) - net.runNetwork() // Clear network map registration messages + clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + mockNet.runNetwork() // Clear network map registration messages } - @Test fun `should sign a unique transaction with a valid timestamp`() { + @Test fun `should sign a unique transaction with a valid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTime(Instant.now(), 30.seconds) + tx.addTimeWindow(Instant.now(), 30.seconds) clientNode.services.signInitialTransaction(tx) } @@ -52,7 +52,7 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should sign a unique transaction without a timestamp`() { + @Test fun `should sign a unique transaction without a time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) @@ -64,18 +64,18 @@ class NotaryServiceTests { signatures.forEach { it.verify(stx.id) } } - @Test fun `should report error for transaction with an invalid timestamp`() { + @Test fun `should report error for transaction with an invalid time-window`() { val stx = run { val inputState = issueState(clientNode) val tx = TransactionType.General.Builder(notaryNode.info.notaryIdentity).withItems(inputState) - tx.setTime(Instant.now().plusSeconds(3600), 30.seconds) + tx.addTimeWindow(Instant.now().plusSeconds(3600), 30.seconds) clientNode.services.signInitialTransaction(tx) } val future = runNotaryClient(stx) val ex = assertFailsWith(NotaryException::class) { future.getOrThrow() } - assertThat(ex.error).isInstanceOf(NotaryError.TimestampInvalid::class.java) + assertThat(ex.error).isInstanceOf(NotaryError.TimeWindowInvalid::class.java) } @Test fun `should sign identical transaction multiple times (signing is idempotent)`() { @@ -90,7 +90,7 @@ class NotaryServiceTests { val f1 = clientNode.services.startFlow(firstAttempt) val f2 = clientNode.services.startFlow(secondAttempt) - net.runNetwork() + mockNet.runNetwork() assertEquals(f1.resultFuture.getOrThrow(), f2.resultFuture.getOrThrow()) } @@ -112,7 +112,7 @@ class NotaryServiceTests { clientNode.services.startFlow(firstSpend) val future = clientNode.services.startFlow(secondSpend) - net.runNetwork() + mockNet.runNetwork() val ex = assertFailsWith(NotaryException::class) { future.resultFuture.getOrThrow() } val notaryError = ex.error as NotaryError.Conflict @@ -123,7 +123,7 @@ class NotaryServiceTests { private fun runNotaryClient(stx: SignedTransaction): ListenableFuture> { val flow = NotaryFlow.Client(stx) val future = clientNode.services.startFlow(flow).resultFuture - net.runNetwork() + mockNet.runNetwork() return future } diff --git a/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt b/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt index 979a4f794a..baec774fa0 100644 --- a/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt +++ b/node/src/test/kotlin/net/corda/node/services/transactions/PathManagerTests.kt @@ -8,9 +8,11 @@ import kotlin.test.assertFalse import kotlin.test.assertTrue class PathManagerTests { + private class MyPathManager : PathManager(Files.createTempFile(MyPathManager::class.simpleName, null)) + @Test fun `path deleted when manager closed`() { - val manager = PathManager(Files.createTempFile(javaClass.simpleName, null)) + val manager = MyPathManager() val leakedPath = manager.use { it.path.also { assertTrue(it.exists()) } } @@ -20,7 +22,7 @@ class PathManagerTests { @Test fun `path deleted when handle closed`() { - val handle = PathManager(Files.createTempFile(javaClass.simpleName, null)).use { + val handle = MyPathManager().use { it.handle() } val leakedPath = handle.use { 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 dfe2cc29fa..aa8a417d81 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 @@ -23,18 +23,18 @@ import kotlin.test.assertEquals import kotlin.test.assertFailsWith class ValidatingNotaryServiceTests { - lateinit var net: MockNetwork + lateinit var mockNet: MockNetwork lateinit var notaryNode: MockNetwork.MockNode lateinit var clientNode: MockNetwork.MockNode @Before fun setup() { - net = MockNetwork() - notaryNode = net.createNode( + mockNet = MockNetwork() + notaryNode = mockNet.createNode( legalName = DUMMY_NOTARY.name, advertisedServices = *arrayOf(ServiceInfo(NetworkMapService.type), ServiceInfo(ValidatingNotaryService.type)) ) - clientNode = net.createNode(networkMapAddress = notaryNode.info.address) - net.runNetwork() // Clear network map registration messages + clientNode = mockNet.createNode(networkMapAddress = notaryNode.info.address) + mockNet.runNetwork() // Clear network map registration messages } @Test fun `should report error for invalid transaction dependency`() { @@ -74,7 +74,7 @@ class ValidatingNotaryServiceTests { private fun runClient(stx: SignedTransaction): ListenableFuture> { val flow = NotaryFlow.Client(stx) val future = clientNode.services.startFlow(flow).resultFuture - net.runNetwork() + mockNet.runNetwork() return future } 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 db8374cf34..74df33c1a9 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 @@ -1,15 +1,16 @@ package net.corda.node.services.vault import net.corda.contracts.CommercialPaper +import net.corda.contracts.DealState import net.corda.contracts.asset.Cash import net.corda.contracts.asset.DUMMY_CASH_ISSUER import net.corda.contracts.testing.fillWithSomeTestCash import net.corda.contracts.testing.fillWithSomeTestDeals import net.corda.contracts.testing.fillWithSomeTestLinearStates import net.corda.core.contracts.* -import net.corda.core.identity.Party import net.corda.core.crypto.entropyToKeyPair import net.corda.core.days +import net.corda.core.identity.Party import net.corda.core.node.services.Vault import net.corda.core.node.services.VaultService import net.corda.core.node.services.linearHeadsOfType @@ -207,7 +208,7 @@ class VaultQueryTests { database.transaction { 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("456"), participants = listOf(MEGA_CORP, BIG_CORP)) services.fillWithSomeTestDeals(listOf("123", "789"), participants = listOf(BIG_CORP, MINI_CORP)) // DOCSTART VaultQueryExample5 @@ -533,7 +534,7 @@ class VaultQueryTests { database.transaction { services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) - services.fillWithSomeTestDeals(listOf("456"), 3) // create 3 revisions with same ID + services.fillWithSomeTestDeals(listOf("456")) // create 3 revisions with same ID services.fillWithSomeTestDeals(listOf("123", "789")) val criteria = LinearStateQueryCriteria(dealRef = listOf("456"), latestOnly = true) @@ -547,7 +548,7 @@ class VaultQueryTests { database.transaction { services.fillWithSomeTestLinearStates(2, UniqueIdentifier("TEST")) - services.fillWithSomeTestDeals(listOf("456"), 3) // specify party + services.fillWithSomeTestDeals(listOf("456")) // specify party services.fillWithSomeTestDeals(listOf("123", "789")) // DOCSTART VaultQueryExample11 @@ -701,7 +702,7 @@ class VaultQueryTests { val issuance = MEGA_CORP.ref(1) val commercialPaper = CommercialPaper().generateIssue(issuance, faceValue, TEST_TX_TIME + 30.days, DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) + addTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(DUMMY_NOTARY_KEY) }.toSignedTransaction() diff --git a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt index c1d47292f8..1a2dfa6569 100644 --- a/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt +++ b/node/src/test/kotlin/net/corda/node/utilities/registration/NetworkisRegistrationHelperTest.kt @@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.eq import com.nhaarman.mockito_kotlin.mock import net.corda.core.crypto.* import net.corda.core.exists +import net.corda.core.toTypedArray import net.corda.core.utilities.ALICE import net.corda.testing.TestNodeConfiguration import net.corda.testing.getTestX509Name @@ -29,8 +30,8 @@ class NetworkRegistrationHelperTest { "CORDA_INTERMEDIATE_CA", "CORDA_ROOT_CA") .map { getTestX509Name(it) } - val certs = identities.map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } - .toTypedArray() + val certs = identities.stream().map { X509Utilities.createSelfSignedCACertificate(it, Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)) } + .map { it.cert }.toTypedArray() val certService: NetworkRegistrationService = mock { on { submitRequest(any()) }.then { id } diff --git a/samples/attachment-demo/build.gradle b/samples/attachment-demo/build.gradle index 54f265af43..936586894e 100644 --- a/samples/attachment-demo/build.gradle +++ b/samples/attachment-demo/build.gradle @@ -26,8 +26,8 @@ dependencies { testCompile "junit:junit:$junit_version" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':test-utils') @@ -42,13 +42,12 @@ dependencies { } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { - ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.flows.FinalityFlow"]]] + ext.rpcUsers = [['username': "demo", 'password': "demo", 'permissions': ["StartFlow.net.corda.attachmentdemo.AttachmentDemoFlow"]]] directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" advertisedServices["corda.notary.validating"] p2pPort 10002 rpcPort 10003 @@ -56,8 +55,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=Bank A,O=Bank A,L=London,C=UK" - nearestCity "London" + name "CN=Bank A,O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -66,7 +64,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "CN=Bank B,O=Bank B,L=New York,C=US" - nearestCity "New York" advertisedServices = [] p2pPort 10008 rpcPort 10009 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 0b9ca23c58..61f57c93b0 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 @@ -6,7 +6,7 @@ import net.corda.core.node.services.ServiceInfo 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.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User 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 745d83e4cf..739bebc8f8 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 @@ -8,23 +8,22 @@ import net.corda.core.contracts.Contract import net.corda.core.contracts.ContractState 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.identity.Party import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startTrackedFlow import net.corda.core.sizedInputStreamAndHash import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.* import net.corda.flows.FinalityFlow -import net.corda.node.driver.poll +import net.corda.testing.driver.poll import java.io.InputStream import java.net.HttpURLConnection import java.net.URL -import java.security.PublicKey import java.util.concurrent.Executors import java.util.jar.JarInputStream import javax.servlet.http.HttpServletResponse.SC_OK @@ -170,7 +169,7 @@ private fun printHelp(parser: OptionParser) { class AttachmentContract : Contract { override val legalContractReference: SecureHash - get() = TODO("not implemented") + get() = SecureHash.zeroHash // TODO not implemented override fun verify(tx: TransactionForContract) { val state = tx.outputs.filterIsInstance().single() 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 9e7485eda3..bfd97e359a 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 @@ -5,9 +5,9 @@ import net.corda.core.node.services.ServiceInfo 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.transactions.SimpleNotaryService import net.corda.nodeapi.User +import net.corda.testing.driver.driver /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) diff --git a/samples/bank-of-corda-demo/build.gradle b/samples/bank-of-corda-demo/build.gradle index 39ea0da9ec..fd957bbcdc 100644 --- a/samples/bank-of-corda-demo/build.gradle +++ b/samples/bank-of-corda-demo/build.gradle @@ -26,8 +26,8 @@ dependencies { testCompile "junit:junit:$junit_version" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':client:jfx') compile project(':client:rpc') @@ -42,18 +42,16 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into BankOfCordaClientApi so if you change it here, change it there too. // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH" advertisedServices = ["corda.notary.validating"] p2pPort 10002 rpcPort 10003 cordapps = [] } node { - name "CN=BankOfCorda,O=R3,OU=Corda QA Department,L=New York,C=US" - nearestCity "New York" + name "CN=BankOfCorda,O=R3,L=New York,C=US" advertisedServices = ["corda.issuer.USD"] p2pPort 10005 rpcPort 10006 @@ -63,12 +61,12 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { ['username' : "bankUser", 'password' : "test", 'permissions': ["StartFlow.net.corda.flows.CashPaymentFlow", + "StartFlow.net.corda.flows.CashExitFlow", "StartFlow.net.corda.flows.IssuerFlow\$IssuanceRequester"]] ] } node { - name "CN=BigCorporation,O=R3,OU=corda,L=London,C=UK" - nearestCity "New York" + name "CN=BigCorporation,O=R3,OU=corda,L=London,C=GB" advertisedServices = [] p2pPort 10008 rpcPort 10009 diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt index 06192cca30..2887b84435 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaHttpAPITest.kt @@ -5,11 +5,9 @@ import net.corda.bank.api.BankOfCordaClientApi import net.corda.bank.api.BankOfCordaWebApi.IssueRequestParams import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo -import net.corda.node.driver.driver +import net.corda.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.BOC -import net.corda.testing.http.HttpUtils -import org.bouncycastle.asn1.x500.X500Name import org.junit.Test import kotlin.test.assertTrue diff --git a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt index 6a41c6546e..7609ce2bb9 100644 --- a/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt +++ b/samples/bank-of-corda-demo/src/integration-test/kotlin/net/corda/bank/BankOfCordaRPCClientTest.kt @@ -6,7 +6,7 @@ import net.corda.core.getOrThrow import net.corda.core.messaging.startFlow import net.corda.core.node.services.ServiceInfo import net.corda.flows.IssuerFlow.IssuanceRequester -import net.corda.node.driver.driver +import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User diff --git a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt index 86f197a824..8c54ac4804 100644 --- a/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt +++ b/samples/bank-of-corda-demo/src/main/kotlin/net/corda/bank/BankOfCordaDriver.kt @@ -9,9 +9,10 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.flows.CashExitFlow import net.corda.flows.CashPaymentFlow import net.corda.flows.IssuerFlow -import net.corda.node.driver.driver +import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -29,7 +30,7 @@ fun main(args: Array) { val BANK_USERNAME = "bankUser" val BIGCORP_USERNAME = "bigCorpUser" -val BIGCORP_LEGAL_NAME = X500Name("CN=BigCorporation,O=R3,OU=corda,L=London,C=UK") +val BIGCORP_LEGAL_NAME = X500Name("CN=BigCorporation,O=R3,OU=corda,L=London,C=GB") private class BankOfCordaDriver { enum class Role { @@ -57,7 +58,7 @@ private class BankOfCordaDriver { val role = options.valueOf(roleArg)!! if (role == Role.ISSUER) { driver(dsl = { - val bankUser = User(BANK_USERNAME, "test", permissions = setOf(startFlowPermission(), startFlowPermission())) + val bankUser = User(BANK_USERNAME, "test", permissions = setOf(startFlowPermission(), startFlowPermission(), startFlowPermission())) val bigCorpUser = User(BIGCORP_USERNAME, "test", permissions = setOf(startFlowPermission())) startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) val bankOfCorda = startNode(BOC.name, rpcUsers = listOf(bankUser), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.USD")))) 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 8fd4ce47bb..7708ee18e5 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,13 +1,10 @@ package net.corda.bank.plugin import net.corda.bank.api.BankOfCordaWebApi -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry -import net.corda.flows.IssuerFlow +import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function -class BankOfCordaPlugin : CordaPluginRegistry() { +class BankOfCordaPlugin : WebServerPluginRegistry { // A list of classes that expose web APIs. override val webApis = listOf(Function(::BankOfCordaWebApi)) - override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) } diff --git a/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index b4dddc2d14..0000000000 --- a/samples/bank-of-corda-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.bank.plugin.BankOfCordaPlugin diff --git a/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry b/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry new file mode 100644 index 0000000000..7ecf28dd36 --- /dev/null +++ b/samples/bank-of-corda-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -0,0 +1,2 @@ +# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry +net.corda.bank.plugin.BankOfCordaPlugin \ No newline at end of file diff --git a/samples/irs-demo/build.gradle b/samples/irs-demo/build.gradle index 1f45c1903f..33587b10da 100644 --- a/samples/irs-demo/build.gradle +++ b/samples/irs-demo/build.gradle @@ -26,16 +26,13 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') compile project(':webserver') - compile project(':test-utils') // Javax is required for webapis compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" @@ -43,14 +40,17 @@ dependencies { // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps compile "com.squareup.okhttp3:okhttp:$okhttp_version" + + testCompile project(':test-utils') + testCompile "junit:junit:$junit_version" + testCompile "org.assertj:assertj-core:${assertj_version}" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating", "corda.interest_rates"] p2pPort 10002 rpcPort 10003 @@ -59,8 +59,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { useTestClock true } node { - name "CN=Bank A,O=Bank A,L=London,C=UK" - nearestCity "London" + name "CN=Bank A,O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -70,7 +69,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "CN=Bank B,O=Bank B,L=New York,C=US" - nearestCity "New York" advertisedServices = [] p2pPort 10008 rpcPort 10009 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 01b0a588b0..c68008daf8 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 @@ -5,99 +5,126 @@ import com.google.common.util.concurrent.Futures import net.corda.client.rpc.CordaRPCClient import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo +import net.corda.core.toFuture 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.irs.api.NodeInterestRates import net.corda.irs.contract.InterestRateSwap -import net.corda.irs.utilities.postJson -import net.corda.irs.utilities.putJson import net.corda.irs.utilities.uploadFile -import net.corda.node.driver.driver import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.IntegrationTestCategory +import net.corda.testing.driver.driver import net.corda.testing.http.HttpApi import org.apache.commons.io.IOUtils import org.assertj.core.api.Assertions.assertThat import org.junit.Test -import rx.observables.BlockingObservable +import rx.Observable import java.net.URL +import java.time.Duration import java.time.LocalDate +import java.time.temporal.ChronoUnit class IRSDemoTest : IntegrationTestCategory { val rpcUser = User("user", "password", emptySet()) val currentDate: LocalDate = LocalDate.now() val futureDate: LocalDate = currentDate.plusMonths(6) + val maxWaitTime: Duration = Duration.of(60, ChronoUnit.SECONDS) @Test fun `runs IRS demo`() { driver(useTestClock = true, isDebug = true) { val (controller, nodeA, nodeB) = Futures.allAsList( - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))), + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), startNode(DUMMY_BANK_A.name, rpcUsers = listOf(rpcUser)), startNode(DUMMY_BANK_B.name) ).getOrThrow() + println("All nodes started") + val (controllerAddr, nodeAAddr, nodeBAddr) = Futures.allAsList( startWebserver(controller), startWebserver(nodeA), startWebserver(nodeB) ).getOrThrow().map { it.listenAddress } + println("All webservers started") + + val (_, nodeAApi, nodeBApi) = listOf(controller, nodeA, nodeB).zip(listOf(controllerAddr, nodeAAddr, nodeBAddr)).map { + val mapper = net.corda.jackson.JacksonSupport.createDefaultMapper(it.first.rpc) + HttpApi.fromHostAndPort(it.second, "api/irs", mapper = mapper) + } val nextFixingDates = getFixingDateObservable(nodeA.configuration) - val numADeals = getTradeCount(nodeAAddr) - val numBDeals = getTradeCount(nodeBAddr) + val numADeals = getTradeCount(nodeAApi) + val numBDeals = getTradeCount(nodeBApi) runUploadRates(controllerAddr) - runTrade(nodeAAddr) + runTrade(nodeAApi) - assertThat(getTradeCount(nodeAAddr)).isEqualTo(numADeals + 1) - assertThat(getTradeCount(nodeBAddr)).isEqualTo(numBDeals + 1) + assertThat(getTradeCount(nodeAApi)).isEqualTo(numADeals + 1) + assertThat(getTradeCount(nodeBApi)).isEqualTo(numBDeals + 1) + assertThat(getFloatingLegFixCount(nodeAApi) == 0) // Wait until the initial trade and all scheduled fixings up to the current date have finished - nextFixingDates.first { it == null || it > currentDate } + nextFixingDates.firstWithTimeout(maxWaitTime){ it == null || it > currentDate } + runDateChange(nodeBApi) + nextFixingDates.firstWithTimeout(maxWaitTime) { it == null || it > futureDate } - runDateChange(nodeBAddr) - nextFixingDates.first { it == null || it > futureDate } + assertThat(getFloatingLegFixCount(nodeAApi) > 0) } } - fun getFixingDateObservable(config: FullNodeConfiguration): BlockingObservable { + fun getFloatingLegFixCount(nodeApi: HttpApi) = getTrades(nodeApi)[0].calculation.floatingLegPaymentSchedule.count { it.value.rate.ratioUnit != null } + + fun getFixingDateObservable(config: FullNodeConfiguration): Observable { val client = CordaRPCClient(config.rpcAddress!!) val proxy = client.start("user", "password").proxy val vaultUpdates = proxy.vaultAndUpdates().second - val fixingDates = vaultUpdates.map { update -> + return vaultUpdates.map { update -> val irsStates = update.produced.map { it.state.data }.filterIsInstance() irsStates.mapNotNull { it.calculation.nextFixingDate() }.max() - }.cache().toBlocking() - - return fixingDates + }.cache() } - private fun runDateChange(nodeAddr: HostAndPort) { - val url = URL("http://$nodeAddr/api/irs/demodate") - assertThat(putJson(url, "\"$futureDate\"")).isTrue() + private fun runDateChange(nodeApi: HttpApi) { + println("Running date change against ${nodeApi.root}") + assertThat(nodeApi.putJson("demodate", "\"$futureDate\"")).isTrue() } - private fun runTrade(nodeAddr: HostAndPort) { - val fileContents = IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream("net/corda/irs/simulation/example-irs-trade.json"), Charsets.UTF_8.name()) + private fun runTrade(nodeApi: HttpApi) { + println("Running trade against ${nodeApi.root}") + val fileContents = loadResourceFile("net/corda/irs/simulation/example-irs-trade.json") val tradeFile = fileContents.replace("tradeXXX", "trade1") - val url = URL("http://$nodeAddr/api/irs/deals") - assertThat(postJson(url, tradeFile)).isTrue() + assertThat(nodeApi.postJson("deals", tradeFile)).isTrue() } private fun runUploadRates(host: HostAndPort) { - 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") + println("Running upload rates against $host") + val fileContents = loadResourceFile("net/corda/irs/simulation/example.rates.txt") + val url = URL("http://$host/api/irs/fixes") assertThat(uploadFile(url, fileContents)).isTrue() } - private fun getTradeCount(nodeAddr: HostAndPort): Int { - val api = HttpApi.fromHostAndPort(nodeAddr, "api/irs") - val deals = api.getJson>("deals") + private fun loadResourceFile(filename: String): String { + return IOUtils.toString(Thread.currentThread().contextClassLoader.getResourceAsStream(filename), Charsets.UTF_8.name()) + } + + private fun getTradeCount(nodeApi: HttpApi): Int { + println("Getting trade count from ${nodeApi.root}") + val deals = nodeApi.getJson>("deals") return deals.size } + + private fun getTrades(nodeApi: HttpApi): Array { + println("Getting trades from ${nodeApi.root}") + val deals = nodeApi.getJson>("deals") + return deals + } + + fun Observable.firstWithTimeout(timeout: Duration, pred: (T) -> Boolean) { + first(pred).toFuture().getOrThrow(timeout) + } } 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 8f09afaebf..29821de3c9 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 @@ -2,18 +2,13 @@ package net.corda.irs.api import net.corda.client.rpc.notUsed import net.corda.core.contracts.filterStatesOfType -import net.corda.core.identity.Party import net.corda.core.getOrThrow import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.startFlow import net.corda.core.utilities.loggerFor import net.corda.irs.contract.InterestRateSwap import net.corda.irs.flows.AutoOfferFlow -import net.corda.irs.flows.UpdateBusinessDayFlow import java.net.URI -import java.time.LocalDate -import java.time.LocalDateTime -import java.time.ZoneId import javax.ws.rs.* import javax.ws.rs.core.MediaType import javax.ws.rs.core.Response @@ -29,8 +24,6 @@ import javax.ws.rs.core.Response * * TODO: where we currently refer to singular external deal reference, of course this could easily be multiple identifiers e.g. CUSIP, ISIN. * - * GET /api/irs/demodate - return the current date as viewed by the system in YYYY-MM-DD format. - * PUT /api/irs/demodate - put date in format YYYY-MM-DD to advance the current date as viewed by the system and * simulate any associated business processing (currently fixing). * * TODO: replace simulated date advancement with business event based implementation @@ -89,28 +82,4 @@ class InterestRateSwapAPI(val rpc: CordaRPCOps) { return Response.ok().entity(deal).build() } } - - @PUT - @Path("demodate") - @Consumes(MediaType.APPLICATION_JSON) - fun storeDemoDate(newDemoDate: LocalDate): Response { - val priorDemoDate = fetchDemoDate() - // Can only move date forwards - if (newDemoDate.isAfter(priorDemoDate)) { - // TODO: Remove this suppress when we upgrade to kotlin 1.1 or when JetBrain fixes the bug. - @Suppress("UNSUPPORTED_FEATURE") - rpc.startFlow(UpdateBusinessDayFlow::Broadcast, newDemoDate).returnValue.getOrThrow() - return Response.ok().build() - } - val msg = "demodate is already $priorDemoDate and can only be updated with a later date" - logger.error("Attempt to set demodate to $newDemoDate but $msg") - return Response.status(Response.Status.CONFLICT).entity(msg).build() - } - - @GET - @Path("demodate") - @Produces(MediaType.APPLICATION_JSON) - fun fetchDemoDate(): LocalDate { - return LocalDateTime.ofInstant(rpc.currentNodeTime(), ZoneId.systemDefault()).toLocalDate() - } } 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 b485f406a1..f3791bbe64 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 @@ -1,44 +1,38 @@ package net.corda.irs.api import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.BusinessCalendar +import net.corda.contracts.Fix +import net.corda.contracts.FixOf +import net.corda.contracts.Tenor +import net.corda.contracts.math.CubicSplineInterpolator +import net.corda.contracts.math.Interpolator +import net.corda.contracts.math.InterpolatorFactory import net.corda.core.RetryableException -import net.corda.core.contracts.* +import net.corda.core.ThreadBox +import net.corda.core.contracts.Command import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.MerkleTreeException import net.corda.core.crypto.keys -import net.corda.core.crypto.sign import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.flows.StartableByRPC 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.CordaService 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.RatesFixFlow -import net.corda.node.services.api.AcceptsFileUpload -import net.corda.node.utilities.AbstractJDBCHashSet -import net.corda.node.utilities.FiberBox -import net.corda.node.utilities.JDBCHashedTable -import net.corda.node.utilities.localDate -import org.jetbrains.exposed.sql.ResultRow -import org.jetbrains.exposed.sql.statements.InsertStatement -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.HashSet import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set @@ -53,135 +47,85 @@ import kotlin.collections.set * for signing. */ object NodeInterestRates { - val type = ServiceType.corda.getSubType("interest_rates") - - /** - * Register the flow that is used with the Fixing integration tests. - */ - class Plugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(::Service)) - } - - /** - * The Service that wraps [Oracle] and handles messages/network interaction/request scrubbing. - */ // DOCSTART 2 - class Service(val services: PluginServiceHub) : AcceptsFileUpload, SingletonSerializeAsToken() { - val oracle: Oracle by lazy { - val myNodeInfo = services.myInfo - val myIdentity = myNodeInfo.serviceIdentities(type).first() - val mySigningKey = myIdentity.owningKey.keys.first { services.keyManagementService.keys.contains(it) } - Oracle(myIdentity, mySigningKey, services) - } - - init { - // Note: access to the singleton oracle property is via the registered SingletonSerializeAsToken Service. - // Otherwise the Kryo serialisation of the call stack in the Quasar Fiber extends to include - // the framework Oracle and the flow will crash. - services.registerServiceFlow(RatesFixFlow.FixSignFlow::class.java) { FixSignHandler(it, this) } - services.registerServiceFlow(RatesFixFlow.FixQueryFlow::class.java) { FixQueryHandler(it, this) } - } - - private class FixSignHandler(val otherParty: Party, val service: Service) : FlowLogic() { - @Suspendable - override fun call() { - val request = receive(otherParty).unwrap { it } - send(otherParty, service.oracle.sign(request.ftx)) - } - } - - private class FixQueryHandler(val otherParty: Party, val service: Service) : FlowLogic() { - companion object { - object RECEIVED : ProgressTracker.Step("Received fix request") - object SENDING : ProgressTracker.Step("Sending fix response") - } - - override val progressTracker = ProgressTracker(RECEIVED, SENDING) - - init { - progressTracker.currentStep = RECEIVED - } - - @Suspendable - override fun call(): Unit { - val request = receive(otherParty).unwrap { it } - val answers = service.oracle.query(request.queries, request.deadline) - progressTracker.currentStep = SENDING - send(otherParty, answers) - } - } - // DOCEND 2 - - // File upload support - override val dataTypePrefix = "interest-rates" - override val acceptableFileExtensions = listOf(".rates", ".txt") - - override fun upload(file: InputStream): String { - val fixes = parseFile(file.bufferedReader().readText()) - oracle.knownFixes = fixes - val msg = "Interest rates oracle accepted ${fixes.size} new interest rate fixes" - println(msg) - return msg + @InitiatedBy(RatesFixFlow.FixSignFlow::class) + class FixSignHandler(val otherParty: Party) : FlowLogic() { + @Suspendable + override fun call() { + val request = receive(otherParty).unwrap { it } + val oracle = serviceHub.cordaService(Oracle::class.java) + send(otherParty, oracle.sign(request.ftx)) } } + @InitiatedBy(RatesFixFlow.FixQueryFlow::class) + class FixQueryHandler(val otherParty: Party) : FlowLogic() { + object RECEIVED : ProgressTracker.Step("Received fix request") + object SENDING : ProgressTracker.Step("Sending fix response") + + override val progressTracker = ProgressTracker(RECEIVED, SENDING) + + @Suspendable + override fun call(): Unit { + val request = receive(otherParty).unwrap { it } + progressTracker.currentStep = RECEIVED + val oracle = serviceHub.cordaService(Oracle::class.java) + val answers = oracle.query(request.queries) + progressTracker.currentStep = SENDING + send(otherParty, answers) + } + } + // DOCEND 2 + /** * An implementation of an interest rate fix oracle which is given data in a simple string format. * * 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: PublicKey, val services: ServiceHub) { - private object Table : JDBCHashedTable("demo_interest_rate_fixes") { - val name = varchar("index_name", length = 255) - val forDay = localDate("for_day") - val ofTenor = varchar("of_tenor", length = 16) - val value = decimal("value", scale = 20, precision = 16) + // DOCSTART 3 + @CordaService + class Oracle(val identity: Party, private val signingKey: PublicKey, val services: ServiceHub) : SingletonSerializeAsToken() { + constructor(services: PluginServiceHub) : this( + services.myInfo.serviceIdentities(type).first(), + services.myInfo.serviceIdentities(type).first().owningKey.keys.first { services.keyManagementService.keys.contains(it) }, + services + ) + // DOCEND 3 + + companion object { + @JvmField + val type = ServiceType.corda.getSubType("interest_rates") } private class InnerState { - val fixes = object : AbstractJDBCHashSet(Table) { - override fun elementFromRow(row: ResultRow): Fix { - return Fix(FixOf(row[table.name], row[table.forDay], Tenor(row[table.ofTenor])), row[table.value]) - } - - override fun addElementToInsert(insert: InsertStatement, entry: Fix, finalizables: MutableList<() -> Unit>) { - insert[table.name] = entry.of.name - insert[table.forDay] = entry.of.forDay - insert[table.ofTenor] = entry.of.ofTenor.name - insert[table.value] = entry.value - } - } + // TODO Update this to use a database once we have an database API + val fixes = HashSet() var container: FixContainer = FixContainer(fixes) } - private val mutex = FiberBox(InnerState()) + private val mutex = ThreadBox(InnerState()) var knownFixes: FixContainer set(value) { require(value.size > 0) - mutex.write { + mutex.locked { fixes.clear() fixes.addAll(value.fixes) container = value } } - get() = mutex.read { container } + get() = mutex.locked { container } // Make this the last bit of initialisation logic so fully constructed when entered into instances map init { require(signingKey in identity.owningKey.keys) } - /** - * This method will now wait until the given deadline if the fix for the given [FixOf] is not immediately - * available. To implement this, [readWithDeadline] will loop if the deadline is not reached and we throw - * [UnknownFix] as it implements [RetryableException] which has special meaning to this function. - */ @Suspendable - fun query(queries: List, deadline: Instant): List { + fun query(queries: List): List { require(queries.isNotEmpty()) - return mutex.readWithDeadline(services.clock, deadline) { + return mutex.locked { val answers: List = queries.map { container[it] } val firstNull = answers.indexOf(null) if (firstNull != -1) { @@ -231,11 +175,22 @@ object NodeInterestRates { return DigitalSignature.LegallyIdentifiable(identity, signature.bytes) } // DOCEND 1 + + fun uploadFixes(s: String) { + knownFixes = parseFile(s) + } } // TODO: can we split into two? Fix not available (retryable/transient) and unknown (permanent) class UnknownFix(val fix: FixOf) : RetryableException("Unknown fix: $fix") + // Upload the raw fix data via RPC. In a real system the oracle data would be taken from a database. + @StartableByRPC + class UploadFixesFlow(val s: String) : FlowLogic() { + @Suspendable + override fun call() = serviceHub.cordaService(Oracle::class.java).uploadFixes(s) + } + /** Fix container, for every fix name & date pair stores a tenor to interest rate map - [InterpolatingRateMap] */ class FixContainer(val fixes: Set, val factory: InterpolatorFactory = CubicSplineInterpolator) { private val container = buildContainer(fixes) @@ -316,17 +271,21 @@ object NodeInterestRates { map(String::trim). // Filter out comment and empty lines. filterNot { it.startsWith("#") || it.isBlank() }. - map { parseFix(it) }. + map(this::parseFix). toSet() return FixContainer(fixes) } /** Parses a string of the form "LIBOR 16-March-2016 1M = 0.678" into a [Fix] */ - fun parseFix(s: String): Fix { - val (key, value) = s.split('=').map(String::trim) - val of = parseFixOf(key) - val rate = BigDecimal(value) - return Fix(of, rate) + private fun parseFix(s: String): Fix { + try { + val (key, value) = s.split('=').map(String::trim) + val of = parseFixOf(key) + val rate = BigDecimal(value) + return Fix(of, rate) + } catch (e: Exception) { + throw IllegalArgumentException("Unable to parse fix $s: ${e.message}", e) + } } /** Parses a string of the form "LIBOR 16-March-2016 1M" into a [FixOf] */ 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 4c21148852..bc445b4077 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 @@ -1,16 +1,20 @@ package net.corda.irs.contract +import net.corda.contracts.* +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty import net.corda.core.contracts.* import net.corda.core.contracts.clauses.* 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 import net.corda.core.identity.Party import net.corda.core.node.services.ServiceType import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.TransactionBuilder +import net.corda.irs.api.NodeInterestRates import net.corda.irs.flows.FixingFlow import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow import org.apache.commons.jexl3.JexlBuilder @@ -106,6 +110,7 @@ abstract class RatePaymentEvent(date: LocalDate, * Assumes that the rate is valid. */ @CordaSerializable +@JsonIgnoreProperties(ignoreUnknown = true) class FixedRatePaymentEvent(date: LocalDate, accrualStartDate: LocalDate, accrualEndDate: LocalDate, @@ -129,6 +134,7 @@ class FixedRatePaymentEvent(date: LocalDate, * If the rate is null returns a zero payment. // TODO: Is this the desired behaviour? */ @CordaSerializable +@JsonIgnoreProperties(ignoreUnknown = true) class FloatingRatePaymentEvent(date: LocalDate, accrualStartDate: LocalDate, accrualEndDate: LocalDate, @@ -191,10 +197,6 @@ class FloatingRatePaymentEvent(date: LocalDate, class InterestRateSwap : Contract { override val legalContractReference = SecureHash.sha256("is_this_the_text_of_the_contract ? TBD") - companion object { - val oracleType = ServiceType.corda.getSubType("interest_rates") - } - /** * This Common area contains all the information that is not leg specific. */ @@ -459,7 +461,7 @@ class InterestRateSwap : Contract { fixingCalendar, index, indexSource, indexTenor) } - override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) + override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.TimeWindow(), Clauses.Group()), tx.commands.select()) interface Clauses { /** @@ -515,13 +517,13 @@ class InterestRateSwap : Contract { } } - class Timestamped : Clause() { + class TimeWindow : Clause() { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: Unit?): Set { - require(tx.timestamp?.midpoint != null) { "must be timestamped" } + require(tx.timeWindow?.midpoint != null) { "must be have a time-window)" } // We return an empty set because we don't process any commands return emptySet() } @@ -655,6 +657,7 @@ class InterestRateSwap : Contract { /** * The state class contains the 4 major data classes. */ + @JsonIgnoreProperties("parties", "participants", ignoreUnknown = true) data class State( val fixedLeg: FixedLeg, val floatingLeg: FloatingLeg, @@ -666,25 +669,22 @@ class InterestRateSwap : Contract { override val contract = IRS_PROGRAM_ID override val oracleType: ServiceType - get() = InterestRateSwap.oracleType + get() = NodeInterestRates.Oracle.type override val ref = common.tradeID override val participants: List - get() = parties + get() = listOf(fixedLeg.fixedRatePayer, floatingLeg.floatingRatePayer) 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, floatingLeg.floatingRatePayer) - override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity? { val nextFixingOf = nextFixingOf() ?: return null // This is perhaps not how we should determine the time point in the business day, but instead expect the schedule to detail some of these aspects - val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).start + val instant = suggestInterestRateAnnouncementTimeWindow(index = nextFixingOf.name, source = floatingLeg.indexSource, date = nextFixingOf.forDay).fromTime!! return ScheduledActivity(flowLogicRefFactory.create(FixingFlow.FixingRoleDecider::class.java, thisStateRef), instant) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt index 809cb7c05f..1401fbc41e 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/contract/IRSUtils.kt @@ -1,7 +1,9 @@ package net.corda.irs.contract +import net.corda.contracts.Tenor +import com.fasterxml.jackson.annotation.JsonIgnore +import com.fasterxml.jackson.annotation.JsonIgnoreProperties import net.corda.core.contracts.Amount -import net.corda.core.contracts.Tenor import net.corda.core.serialization.CordaSerializable import java.math.BigDecimal import java.util.* @@ -36,6 +38,7 @@ val String.percent: PercentageRatioUnit get() = PercentageRatioUnit(this) /** * Parent of the Rate family. Used to denote fixed rates, floating rates, reference rates etc. */ +@JsonIgnoreProperties(ignoreUnknown = true) open class Rate(val ratioUnit: RatioUnit? = null) { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -63,6 +66,7 @@ open class Rate(val ratioUnit: RatioUnit? = null) { */ @CordaSerializable class FixedRate(ratioUnit: RatioUnit) : Rate(ratioUnit) { + @JsonIgnore fun isPositive(): Boolean = ratioUnit!!.value > BigDecimal("0.0") override fun equals(other: Any?) = other?.javaClass == javaClass && super.equals(other) 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 7e7520c65a..f11d5c25b1 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 @@ -1,21 +1,19 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.DealState +import net.corda.contracts.DealState import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy 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 +import net.corda.core.identity.Party import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.ProgressTracker import net.corda.flows.TwoPartyDealFlow import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.Instigator -import java.util.function.Function /** * This whole class is really part of a demo just to initiate the agreement of a deal with a simple @@ -25,18 +23,6 @@ import java.util.function.Function * or the flow would have to reach out to external systems (or users) to verify the deals. */ object AutoOfferFlow { - - class Plugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(::Service)) - } - - - class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { - init { - services.registerServiceFlow(Requester::class.java) { Acceptor(it) } - } - } - @InitiatingFlow @StartableByRPC class Requester(val dealToBeOffered: DealState) : FlowLogic() { @@ -64,7 +50,7 @@ object AutoOfferFlow { require(serviceHub.networkMapCache.notaryNodes.isNotEmpty()) { "No notary nodes registered" } val notary = serviceHub.networkMapCache.notaryNodes.first().notaryIdentity // need to pick which ever party is not us - val otherParty = notUs(dealToBeOffered.parties).map { serviceHub.identityService.partyFromAnonymous(it) }.requireNoNulls().single() + val otherParty = notUs(dealToBeOffered.participants).map { serviceHub.identityService.partyFromAnonymous(it) }.requireNoNulls().single() progressTracker.currentStep = DEALING val myKey = serviceHub.legalIdentityKey val instigator = Instigator( @@ -81,4 +67,7 @@ object AutoOfferFlow { return parties.filter { serviceHub.myInfo.legalIdentity != it } } } + + @InitiatedBy(Requester::class) + class AutoOfferAcceptor(otherParty: Party) : Acceptor(otherParty) } 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 045e56230c..f63e3c3217 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 @@ -1,15 +1,17 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable +import net.corda.contracts.Fix +import net.corda.contracts.FixableDealState import net.corda.core.TransientProperty import net.corda.core.contracts.* import net.corda.core.crypto.toBase58String import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy 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 @@ -22,13 +24,6 @@ import java.math.BigDecimal import java.security.PublicKey object FixingFlow { - - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(FixingRoleDecider::class.java) { Fixer(it) } - } - } - /** * One side of the fixing flow for an interest rate swap, but could easily be generalised further. * @@ -36,8 +31,8 @@ object FixingFlow { * of the flow that is run by the party with the fixed leg of swap deal, which is the basis for deciding * who does what in the flow. */ - class Fixer(override val otherParty: Party, - override val progressTracker: ProgressTracker = TwoPartyDealFlow.Secondary.tracker()) : TwoPartyDealFlow.Secondary() { + @InitiatedBy(FixingRoleDecider::class) + class Fixer(override val otherParty: Party) : TwoPartyDealFlow.Secondary() { private lateinit var txState: TransactionState<*> private lateinit var deal: FixableDealState @@ -51,7 +46,7 @@ object FixingFlow { // validate the party that initiated is the one on the deal and that the recipient corresponds with it. // TODO: this is in no way secure and will be replaced by general session initiation logic in the future // Also check we are one of the parties - require(deal.parties.count { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } == 1) + require(deal.participants.count { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } == 1) return handshake } @@ -62,7 +57,7 @@ object FixingFlow { val fixOf = deal.nextFixingOf()!! // TODO Do we need/want to substitute in new public keys for the Parties? - val myOldParty = deal.parties.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } + val myOldParty = deal.participants.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } val newDeal = deal @@ -77,9 +72,9 @@ object FixingFlow { override fun beforeSigning(fix: Fix) { newDeal.generateFix(ptx, StateAndRef(txState, handshake.payload.ref), fix) - // And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt - // to have one. - ptx.setTime(serviceHub.clock.instant(), 30.seconds) + // And add a request for a time-window: it may be that none of the contracts need this! + // But it can't hurt to have one. + ptx.addTimeWindow(serviceHub.clock.instant(), 30.seconds) } @Suspendable @@ -114,7 +109,7 @@ object FixingFlow { } override val myKey: PublicKey get() { - dealToFix.state.data.parties.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } + dealToFix.state.data.participants.single { it.owningKey == serviceHub.myInfo.legalIdentity.owningKey } return serviceHub.legalIdentityKey } @@ -155,7 +150,7 @@ object FixingFlow { progressTracker.nextStep() val dealToFix = serviceHub.loadState(ref) val fixableDeal = (dealToFix.data as FixableDealState) - val parties = fixableDeal.parties.sortedBy { it.owningKey.toBase58String() } + val parties = fixableDeal.participants.sortedBy { it.owningKey.toBase58String() } val myKey = serviceHub.myInfo.legalIdentity.owningKey if (parties[0].owningKey == myKey) { val fixing = FixingSession(ref, fixableDeal.oracleType) diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt index 2f2ec0641b..61f4256ce4 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/RatesFixFlow.kt @@ -1,22 +1,21 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.contracts.Fix -import net.corda.core.contracts.FixOf +import net.corda.contracts.Fix +import net.corda.contracts.FixOf import net.corda.core.crypto.DigitalSignature -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic import net.corda.core.flows.InitiatingFlow +import net.corda.core.identity.Party import net.corda.core.serialization.CordaSerializable import net.corda.core.transactions.FilteredTransaction import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.irs.flows.RatesFixFlow.FixOutOfRange -import net.corda.irs.utilities.suggestInterestRateAnnouncementTimeWindow import java.math.BigDecimal -import java.time.Instant import java.util.* +import java.util.function.Predicate // This code is unit tested in NodeInterestRates.kt @@ -47,7 +46,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, class FixOutOfRange(@Suppress("unused") val byAmount: BigDecimal) : Exception("Fix out of range by $byAmount") @CordaSerializable - data class QueryRequest(val queries: List, val deadline: Instant) + data class QueryRequest(val queries: List) @CordaSerializable data class SignRequest(val ftx: FilteredTransaction) @@ -62,7 +61,7 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, tx.addCommand(fix, oracle.owningKey) beforeSigning(fix) progressTracker.currentStep = SIGNING - val mtx = tx.toWireTransaction().buildFilteredTransaction({ filtering(it) }) + val mtx = tx.toWireTransaction().buildFilteredTransaction(Predicate { filtering(it) }) val signature = subFlow(FixSignFlow(tx, oracle, mtx)) tx.addSignatureUnchecked(signature) } @@ -98,9 +97,8 @@ open class RatesFixFlow(protected val tx: TransactionBuilder, class FixQueryFlow(val fixOf: FixOf, val oracle: Party) : FlowLogic() { @Suspendable override fun call(): Fix { - val deadline = suggestInterestRateAnnouncementTimeWindow(fixOf.name, oracle.name.toString(), fixOf.forDay).end // TODO: add deadline to receive - val resp = sendAndReceive>(oracle, QueryRequest(listOf(fixOf), deadline)) + val resp = sendAndReceive>(oracle, QueryRequest(listOf(fixOf))) return resp.unwrap { val fix = it.first() 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 343ebccb79..80de4531b1 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,12 @@ package net.corda.irs.plugin -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry import net.corda.irs.api.InterestRateSwapAPI -import net.corda.irs.flows.FixingFlow +import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function -class IRSPlugin : CordaPluginRegistry() { +class IRSPlugin : WebServerPluginRegistry { override val webApis = listOf(Function(::InterestRateSwapAPI)) override val staticServeDirs: Map = mapOf( "irsdemo" to javaClass.classLoader.getResource("irsweb").toExternalForm() ) - override val servicePlugins = listOf(Function(FixingFlow::Service)) } 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 1d6687d4f5..3337730462 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 @@ -1,6 +1,9 @@ package net.corda.irs.utilities -import okhttp3.* +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody import java.net.URL import java.util.concurrent.TimeUnit @@ -24,10 +27,7 @@ fun postJson(url: URL, data: String): Boolean { } fun uploadFile(url: URL, file: String): Boolean { - val body = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("rates", "net/corda/irs/simulation/example.rates.txt", RequestBody.create(MediaType.parse("text/plain"), file)) - .build() + val body = RequestBody.create(MediaType.parse("text/plain; charset=utf-8"), file) return makeRequest(Request.Builder().url(url).post(body).build()) } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt b/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt index c1d9a03a55..54413553a8 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt +++ b/samples/irs-demo/src/main/kotlin/net/corda/irs/utilities/OracleUtils.kt @@ -1,6 +1,6 @@ package net.corda.irs.utilities -import net.corda.core.utilities.TimeWindow +import net.corda.core.contracts.TimeWindow import java.time.* /** @@ -16,5 +16,5 @@ fun suggestInterestRateAnnouncementTimeWindow(index: String, source: String, dat // Here we apply a blanket announcement time of 11:45 London irrespective of source or index val time = LocalTime.of(11, 45) val zoneId = ZoneId.of("Europe/London") - return TimeWindow(ZonedDateTime.of(date, time, zoneId).toInstant(), Duration.ofHours(24)) + return TimeWindow.fromStartAndDuration(ZonedDateTime.of(date, time, zoneId).toInstant(), Duration.ofHours(24)) } 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 deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/samples/irs-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/irs-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index b7e05a9569..0000000000 --- a/samples/irs-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ /dev/null @@ -1,5 +0,0 @@ -# Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.irs.plugin.IRSPlugin -net.corda.irs.api.NodeInterestRates$Plugin -net.corda.irs.flows.AutoOfferFlow$Plugin -net.corda.irs.flows.UpdateBusinessDayFlow$Plugin diff --git a/samples/irs-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry b/samples/irs-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry new file mode 100644 index 0000000000..5d1ad4fd3f --- /dev/null +++ b/samples/irs-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -0,0 +1,2 @@ +# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry +net.corda.irs.plugin.IRSPlugin diff --git a/samples/irs-demo/src/main/resources/irsweb/js/controllers/CreateDeal.js b/samples/irs-demo/src/main/resources/irsweb/js/controllers/CreateDeal.js index c6d4a7f81e..41b30a6172 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/controllers/CreateDeal.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/controllers/CreateDeal.js @@ -6,10 +6,12 @@ define([ 'utils/semantic', 'utils/dayCountBasisLookup', 'services/NodeApi', - 'Deal' + 'Deal', + 'services/HttpErrorHandler' ], (angular, maskedInput, semantic, dayCountBasisLookup, nodeApi, Deal) => { - angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService) { + angular.module('irsViewer').controller('CreateDealController', function CreateDealController($http, $scope, $location, nodeService, httpErrorHandler) { semantic.init($scope, nodeService.isLoading); + let handleHttpFail = httpErrorHandler.createErrorHandler($scope); $scope.dayCountBasisLookup = dayCountBasisLookup; $scope.deal = nodeService.newDeal(); @@ -17,7 +19,7 @@ define([ nodeService.createDeal(new Deal($scope.deal)) .then((tradeId) => $location.path('#/deal/' + tradeId), (resp) => { $scope.formError = resp.data; - }); + }, handleHttpFail); }; $('input.percent').mask("9.999999%", {placeholder: "", autoclear: false}); $('#swapirscolumns').click(() => { diff --git a/samples/irs-demo/src/main/resources/irsweb/js/controllers/Deal.js b/samples/irs-demo/src/main/resources/irsweb/js/controllers/Deal.js index 6bb8d5cdad..2ccaca2e9b 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/controllers/Deal.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/controllers/Deal.js @@ -1,9 +1,19 @@ 'use strict'; -define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => { - angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService) { +define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => { + angular.module('irsViewer').controller('DealController', function DealController($http, $scope, $routeParams, nodeService, httpErrorHandler) { semantic.init($scope, nodeService.isLoading); + let handleHttpFail = httpErrorHandler.createErrorHandler($scope); + let decorateDeal = (deal) => { + let paymentSchedule = deal.calculation.floatingLegPaymentSchedule; + Object.keys(paymentSchedule).map((key, index) => { + const sign = paymentSchedule[key].rate.positive ? 1 : -1; + paymentSchedule[key].ratePercent = paymentSchedule[key].rate.ratioUnit ? (paymentSchedule[key].rate.ratioUnit.value * 100 * sign).toFixed(5) + "%": ""; + }); - nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = deal); + return deal; + }; + + nodeService.getDeal($routeParams.dealId).then((deal) => $scope.deal = decorateDeal(deal), handleHttpFail); }); }); \ No newline at end of file diff --git a/samples/irs-demo/src/main/resources/irsweb/js/controllers/Home.js b/samples/irs-demo/src/main/resources/irsweb/js/controllers/Home.js index 6a99583728..b92d31f1bc 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/controllers/Home.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/controllers/Home.js @@ -1,12 +1,10 @@ 'use strict'; -define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, nodeApi) => { - angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService) { +define(['angular', 'utils/semantic', 'services/NodeApi', 'services/HttpErrorHandler'], (angular, semantic) => { + angular.module('irsViewer').controller('HomeController', function HomeController($http, $scope, nodeService, httpErrorHandler) { semantic.addLoadingModal($scope, nodeService.isLoading); - let handleHttpFail = (resp) => { - $scope.httpError = resp.data - }; + let handleHttpFail = httpErrorHandler.createErrorHandler($scope); $scope.infoMsg = ""; $scope.errorText = ""; @@ -16,8 +14,19 @@ define(['angular', 'utils/semantic', 'services/NodeApi'], (angular, semantic, no $scope.date = newDate }, handleHttpFail); }; + /* Extract the common name from an X500 name */ + $scope.renderX500Name = (x500Name) => { + var name = x500Name + x500Name.split(',').forEach(function(element) { + var keyValue = element.split('='); + if (keyValue[0].toUpperCase() == 'CN') { + name = keyValue[1]; + } + }); + return name; + }; - nodeService.getDate().then((date) => $scope.date = date); - nodeService.getDeals().then((deals) => $scope.deals = deals); + nodeService.getDate().then((date) => $scope.date = date, handleHttpFail); + nodeService.getDeals().then((deals) => $scope.deals = deals, handleHttpFail); }); -}); \ No newline at end of file +}); diff --git a/samples/irs-demo/src/main/resources/irsweb/js/services/HttpErrorHandler.js b/samples/irs-demo/src/main/resources/irsweb/js/services/HttpErrorHandler.js new file mode 100644 index 0000000000..0ac757b5f6 --- /dev/null +++ b/samples/irs-demo/src/main/resources/irsweb/js/services/HttpErrorHandler.js @@ -0,0 +1,17 @@ +'use strict'; + +define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _) => { + angular.module('irsViewer').factory('httpErrorHandler', () => { + return { + createErrorHandler: (scope) => { + return (resp) => { + if(resp.status == -1) { + scope.httpError = "Could not connect to node web server"; + } else { + scope.httpError = resp.data; + } + }; + } + }; + }); +}); \ No newline at end of file diff --git a/samples/irs-demo/src/main/resources/irsweb/js/services/NodeApi.js b/samples/irs-demo/src/main/resources/irsweb/js/services/NodeApi.js index 2d84ae295b..1cfdf401c5 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/services/NodeApi.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/services/NodeApi.js @@ -5,6 +5,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => { return new (function() { let date = new Date(2016, 0, 1, 0, 0, 0); let curLoading = {}; + let serverAddr = ''; // Leave empty to target the same host this page is served from let load = (type, promise) => { curLoading[type] = true; @@ -17,19 +18,20 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => { }); }; + let endpoint = (target) => serverAddr + target; + let changeDateOnNode = (newDate) => { const dateStr = formatDateForNode(newDate); - let endpoint = '/api/irs/demodate'; - return load('date', $http.put(endpoint, "\"" + dateStr + "\"")).then((resp) => { + return load('date', $http.put(endpoint('/api/irs/demodate'), "\"" + dateStr + "\"")).then((resp) => { date = newDate; return this.getDateModel(date); }); }; this.getDate = () => { - return load('date', $http.get('/api/irs/demodate')).then((resp) => { - const parts = resp.data.split("-"); - date = new Date(parts[0], parts[1] - 1, parts[2]); // JS uses 0 based months + return load('date', $http.get(endpoint('/api/irs/demodate'))).then((resp) => { + const dateParts = resp.data; + date = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); // JS uses 0 based months return this.getDateModel(date); }); }; @@ -54,13 +56,13 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => { }; this.getDeals = () => { - return load('deals', $http.get('/api/irs/deals')).then((resp) => { + return load('deals', $http.get(endpoint('/api/irs/deals'))).then((resp) => { return resp.data.reverse(); }); }; this.getDeal = (dealId) => { - return load('deal' + dealId, $http.get('/api/irs/deals/' + dealId)).then((resp) => { + return load('deal' + dealId, $http.get(endpoint('/api/irs/deals/' + dealId))).then((resp) => { // Do some data modification to simplify the model let deal = resp.data; deal.fixedLeg.fixedRate.value = (deal.fixedLeg.fixedRate.ratioUnit.value * 100).toString().slice(0, 6); @@ -87,7 +89,7 @@ define(['angular', 'lodash', 'viewmodel/Deal'], (angular, _, dealViewModel) => { }; this.createDeal = (deal) => { - return load('create-deal', $http.post('/api/irs/deals', deal.toJson())) + return load('create-deal', $http.post(endpoint('/api/irs/deals'), deal.toJson())) .then((resp) => { return deal.tradeId; }, (resp) => { diff --git a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js index 0fb73960ee..a6d25e2ec1 100644 --- a/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js +++ b/samples/irs-demo/src/main/resources/irsweb/js/viewmodel/FixedLeg.js @@ -2,7 +2,7 @@ define(['utils/dayCountBasisLookup'], (dayCountBasisLookup) => { return { - fixedRatePayer: "CN=Bank A,O=Bank A,L=London,C=UK", + fixedRatePayer: "CN=Bank A,O=Bank A,L=London,C=GB", notional: { quantity: 2500000000 }, diff --git a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html index 42b1de25a6..4b3decbf3a 100644 --- a/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html +++ b/samples/irs-demo/src/main/resources/irsweb/view/create-deal.html @@ -1,5 +1,7 @@

+
{{formError}}
+
{{httpError}}

New Deal diff --git a/samples/irs-demo/src/main/resources/irsweb/view/deal.html b/samples/irs-demo/src/main/resources/irsweb/view/deal.html index fae30c99b7..9e2afe97d5 100644 --- a/samples/irs-demo/src/main/resources/irsweb/view/deal.html +++ b/samples/irs-demo/src/main/resources/irsweb/view/deal.html @@ -1,5 +1,6 @@
+
{{httpError}}
@@ -199,6 +200,26 @@ + + +
+
+
+ + Fixings +
+
+ + + + + + + +
{{fixing.fixingDate[0]}}-{{fixing.fixingDate[1]}}-{{fixing.fixingDate[2]}}{{fixing.ratePercent}}
+
+
+
diff --git a/samples/irs-demo/src/main/resources/irsweb/view/home.html b/samples/irs-demo/src/main/resources/irsweb/view/home.html index b543a35c4e..2cf47d95c8 100644 --- a/samples/irs-demo/src/main/resources/irsweb/view/home.html +++ b/samples/irs-demo/src/main/resources/irsweb/view/home.html @@ -1,4 +1,5 @@
+
{{httpError}}
{{infoMsg}}
@@ -46,9 +47,9 @@ {{deal.ref}} - {{deal.fixedLeg.fixedRatePayer}} + {{renderX500Name(deal.fixedLeg.fixedRatePayer)}} {{deal.fixedLeg.notional.quantity | number}} {{deal.fixedLeg.notional.token}} - {{deal.floatingLeg.floatingRatePayer}} + {{renderX500Name(deal.floatingLeg.floatingRatePayer)}} {{deal.floatingLeg.notional.quantity | number}} {{deal.floatingLeg.notional.token}} diff --git a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json index 964fdf4744..829c38d6a5 100644 --- a/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json +++ b/samples/irs-demo/src/main/resources/net/corda/irs/simulation/example-irs-trade.json @@ -1,6 +1,6 @@ { "fixedLeg": { - "fixedRatePayer": "CN=Bank A,O=Bank A,L=London,C=UK", + "fixedRatePayer": "CN=Bank A,O=Bank A,L=London,C=GB", "notional": "€25000000", "paymentFrequency": "SemiAnnual", "effectiveDate": "2016-03-11", diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/IRSDemo.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt similarity index 97% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/IRSDemo.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt index cf61d9d9f2..fa4a2f020a 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/IRSDemo.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IRSDemo.kt @@ -4,7 +4,6 @@ package net.corda.irs import com.google.common.net.HostAndPort import joptsimple.OptionParser -import net.corda.irs.api.IRSDemoClientApi import kotlin.system.exitProcess enum class Role { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt similarity index 90% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt index 2a179ec611..c983115559 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/api/IrsDemoClientApi.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/IrsDemoClientApi.kt @@ -1,4 +1,4 @@ -package net.corda.irs.api +package net.corda.irs import com.google.common.net.HostAndPort import net.corda.irs.utilities.uploadFile @@ -25,8 +25,9 @@ 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("net/corda/irs/simulation/example.rates.txt"), Charsets.UTF_8.name()) - val url = URL("http://$hostAndPort/upload/interest-rates") + val url = URL("http://$hostAndPort/api/irs/fixes") check(uploadFile(url, fileContents)) + println("Rates successfully uploaded!") } private companion object { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/Main.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt similarity index 90% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/Main.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt index 103f016a3c..c229067712 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/Main.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/Main.kt @@ -7,8 +7,8 @@ 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.irs.api.NodeInterestRates -import net.corda.node.driver.driver import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.driver.driver /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) @@ -17,7 +17,7 @@ import net.corda.node.services.transactions.SimpleNotaryService fun main(args: Array) { driver(dsl = { val (controller, nodeA, nodeB) = Futures.allAsList( - startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.type))), + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type), ServiceInfo(NodeInterestRates.Oracle.type))), startNode(DUMMY_BANK_A.name), startNode(DUMMY_BANK_B.name) ).getOrThrow() diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/api/InterestRatesSwapDemoAPI.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/InterestRatesSwapDemoAPI.kt new file mode 100644 index 0000000000..8e9cb6045b --- /dev/null +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/InterestRatesSwapDemoAPI.kt @@ -0,0 +1,55 @@ +package net.corda.irs.api + +import net.corda.core.getOrThrow +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.messaging.startFlow +import net.corda.core.utilities.loggerFor +import net.corda.irs.flows.UpdateBusinessDayFlow +import java.time.LocalDate +import java.time.LocalDateTime +import java.time.ZoneId +import javax.ws.rs.* +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response + +/** + * GET /api/irs/demodate - return the current date as viewed by the system in YYYY-MM-DD format. + * PUT /api/irs/demodate - put date in format YYYY-MM-DD to advance the current date as viewed by the system and + * POST /api/irs/fixes - store the fixing data as a text file + */ +@Path("irs") +class InterestRatesSwapDemoAPI(val rpc: CordaRPCOps) { + companion object { + private val logger = loggerFor() + } + + @PUT + @Path("demodate") + @Consumes(MediaType.APPLICATION_JSON) + fun storeDemoDate(newDemoDate: LocalDate): Response { + val priorDemoDate = fetchDemoDate() + // Can only move date forwards + if (newDemoDate.isAfter(priorDemoDate)) { + rpc.startFlow(UpdateBusinessDayFlow::Broadcast, newDemoDate).returnValue.getOrThrow() + return Response.ok().build() + } + val msg = "demodate is already $priorDemoDate and can only be updated with a later date" + logger.error("Attempt to set demodate to $newDemoDate but $msg") + return Response.status(Response.Status.CONFLICT).entity(msg).build() + } + + @GET + @Path("demodate") + @Produces(MediaType.APPLICATION_JSON) + fun fetchDemoDate(): LocalDate { + return LocalDateTime.ofInstant(rpc.currentNodeTime(), ZoneId.systemDefault()).toLocalDate() + } + + @POST + @Path("fixes") + @Consumes(MediaType.TEXT_PLAIN) + fun storeFixes(file: String): Response { + rpc.startFlow(NodeInterestRates::UploadFixesFlow, file).returnValue.getOrThrow() + return Response.ok().build() + } +} \ No newline at end of file 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/api/NodeInterestRatesTest.kt similarity index 80% rename from samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt index fee15b50d0..4f25c1af8c 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/NodeInterestRatesTest.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/api/NodeInterestRatesTest.kt @@ -1,5 +1,7 @@ -package net.corda.irs.testing +package net.corda.irs.api +import net.corda.contracts.Fix +import net.corda.contracts.FixOf import net.corda.contracts.asset.CASH import net.corda.contracts.asset.Cash import net.corda.contracts.asset.`issued by` @@ -16,7 +18,6 @@ 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 -import net.corda.irs.api.NodeInterestRates import net.corda.irs.flows.RatesFixFlow import net.corda.node.utilities.configureDatabase import net.corda.node.utilities.transaction @@ -34,6 +35,7 @@ import org.junit.Before import org.junit.Test import java.io.Closeable import java.math.BigDecimal +import java.util.function.Predicate import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse @@ -49,10 +51,8 @@ class NodeInterestRatesTest { """.trimIndent()) 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 DUMMY_CASH_ISSUER = Party(X500Name("CN=Cash issuer,O=R3,OU=corda,L=London,C=GB"), DUMMY_CASH_ISSUER_KEY.public) - 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 @@ -72,7 +72,11 @@ class NodeInterestRatesTest { dataSource = dataSourceAndDatabase.first database = dataSourceAndDatabase.second database.transaction { - oracle = NodeInterestRates.Oracle(MEGA_CORP, MEGA_CORP_KEY.public, dummyServices).apply { knownFixes = TEST_DATA } + oracle = NodeInterestRates.Oracle( + MEGA_CORP, + MEGA_CORP_KEY.public, + MockServices(DUMMY_CASH_ISSUER_KEY, MEGA_CORP_KEY) + ).apply { knownFixes = TEST_DATA } } } @@ -85,7 +89,7 @@ class NodeInterestRatesTest { fun `query successfully`() { database.transaction { val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") - val res = oracle.query(listOf(q), clock.instant()) + val res = oracle.query(listOf(q)) assertEquals(1, res.size) assertEquals("0.678".bd, res[0].value) assertEquals(q, res[0].of) @@ -97,7 +101,7 @@ class NodeInterestRatesTest { database.transaction { val q1 = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") val q2 = NodeInterestRates.parseFixOf("LIBOR 2016-03-15 1M") - val e = assertFailsWith { oracle.query(listOf(q1, q2), clock.instant()) } + val e = assertFailsWith { oracle.query(listOf(q1, q2)) } assertEquals(e.fix, q2) } } @@ -106,7 +110,7 @@ class NodeInterestRatesTest { fun `query successfully with interpolated rate`() { database.transaction { val q = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 5M") - val res = oracle.query(listOf(q), clock.instant()) + val res = oracle.query(listOf(q)) assertEquals(1, res.size) Assert.assertEquals(0.7316228, res[0].value.toDouble(), 0.0000001) assertEquals(q, res[0].of) @@ -117,14 +121,14 @@ class NodeInterestRatesTest { fun `rate missing and unable to interpolate`() { database.transaction { val q = NodeInterestRates.parseFixOf("EURIBOR 2016-03-15 3M") - assertFailsWith { oracle.query(listOf(q), clock.instant()) } + assertFailsWith { oracle.query(listOf(q)) } } } @Test fun `empty query`() { database.transaction { - assertFailsWith { oracle.query(emptyList(), clock.instant()) } + assertFailsWith { oracle.query(emptyList()) } } } @@ -140,11 +144,11 @@ class NodeInterestRatesTest { } } - val ftx1 = wtx1.buildFilteredTransaction(::filterAllOutputs) + val ftx1 = wtx1.buildFilteredTransaction(Predicate(::filterAllOutputs)) assertFailsWith { oracle.sign(ftx1) } tx.addCommand(Cash.Commands.Move(), ALICE_PUBKEY) val wtx2 = tx.toWireTransaction() - val ftx2 = wtx2.buildFilteredTransaction { x -> filterCmds(x) } + val ftx2 = wtx2.buildFilteredTransaction(Predicate { x -> filterCmds(x) }) assertFalse(wtx1.id == wtx2.id) assertFailsWith { oracle.sign(ftx2) } } @@ -154,11 +158,11 @@ class NodeInterestRatesTest { fun `sign successfully`() { database.transaction { val tx = makeTX() - val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")), clock.instant()).first() + val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() tx.addCommand(fix, oracle.identity.owningKey) // Sign successfully. val wtx = tx.toWireTransaction() - val ftx = wtx.buildFilteredTransaction { x -> fixCmdFilter(x) } + val ftx = wtx.buildFilteredTransaction(Predicate { x -> fixCmdFilter(x) }) val signature = oracle.sign(ftx) tx.checkAndAddSignature(signature) } @@ -172,7 +176,7 @@ class NodeInterestRatesTest { val badFix = Fix(fixOf, "0.6789".bd) tx.addCommand(badFix, oracle.identity.owningKey) val wtx = tx.toWireTransaction() - val ftx = wtx.buildFilteredTransaction { x -> fixCmdFilter(x) } + val ftx = wtx.buildFilteredTransaction(Predicate { x -> fixCmdFilter(x) }) val e1 = assertFailsWith { oracle.sign(ftx) } assertEquals(fixOf, e1.fix) } @@ -182,7 +186,7 @@ class NodeInterestRatesTest { fun `do not sign too many leaves`() { database.transaction { val tx = makeTX() - val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M")), clock.instant()).first() + val fix = oracle.query(listOf(NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M"))).first() fun filtering(elem: Any): Boolean { return when (elem) { is Command -> oracle.identity.owningKey in elem.signers && elem.value is Fix @@ -192,7 +196,7 @@ class NodeInterestRatesTest { } tx.addCommand(fix, oracle.identity.owningKey) val wtx = tx.toWireTransaction() - val ftx = wtx.buildFilteredTransaction(::filtering) + val ftx = wtx.buildFilteredTransaction(Predicate(::filtering)) assertFailsWith { oracle.sign(ftx) } } } @@ -201,26 +205,28 @@ class NodeInterestRatesTest { fun `empty partial transaction to sign`() { val tx = makeTX() val wtx = tx.toWireTransaction() - val ftx = wtx.buildFilteredTransaction({ false }) + val ftx = wtx.buildFilteredTransaction(Predicate { false }) assertFailsWith { oracle.sign(ftx) } } @Test fun `network tearoff`() { - val net = MockNetwork() - val n1 = net.createNotaryNode() - val n2 = net.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.type)) + val mockNet = MockNetwork() + val n1 = mockNet.createNotaryNode() + val n2 = mockNet.createNode(n1.info.address, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) + n2.registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) + n2.registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) n2.database.transaction { - n2.findService().oracle.knownFixes = TEST_DATA + n2.installCordaService(NodeInterestRates.Oracle::class.java).knownFixes = TEST_DATA } val tx = TransactionType.General.Builder(null) val fixOf = NodeInterestRates.parseFixOf("LIBOR 2016-03-16 1M") - val oracle = n2.info.serviceIdentities(NodeInterestRates.type).first() + val oracle = n2.info.serviceIdentities(NodeInterestRates.Oracle.type).first() val flow = FilteredRatesFlow(tx, oracle, fixOf, "0.675".bd, "0.1".bd) LogHelper.setLevel("rates") - net.runNetwork() + mockNet.runNetwork() val future = n1.services.startFlow(flow).resultFuture - net.runNetwork() + mockNet.runNetwork() future.getOrThrow() // We should now have a valid signature over our tx from the oracle. val fix = tx.toSignedTransaction(true).tx.commands.map { it.value as Fix }.first() @@ -233,7 +239,8 @@ class NodeInterestRatesTest { fixOf: FixOf, expectedRate: BigDecimal, rateTolerance: BigDecimal, - progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { + progressTracker: ProgressTracker = RatesFixFlow.tracker(fixOf.name)) + : RatesFixFlow(tx, oracle, fixOf, expectedRate, rateTolerance, progressTracker) { override fun filtering(elem: Any): Boolean { return when (elem) { is Command -> oracle.owningKey in elem.signers && elem.value is Fix @@ -242,5 +249,6 @@ class NodeInterestRatesTest { } } - private fun makeTX() = TransactionType.General.Builder(DUMMY_NOTARY).withItems(1000.DOLLARS.CASH `issued by` DUMMY_CASH_ISSUER `owned by` ALICE `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/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt similarity index 96% rename from samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt index 6ca77b8058..78b2e7974e 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSTests.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/contract/IRSTests.kt @@ -1,13 +1,12 @@ -package net.corda.irs.testing +package net.corda.irs.contract +import net.corda.contracts.* import net.corda.core.contracts.* -import net.corda.core.identity.AnonymousParty 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.TEST_TX_TIME -import net.corda.irs.contract.* import net.corda.testing.* import net.corda.testing.node.MockServices import org.junit.Test @@ -224,7 +223,7 @@ class IRSTests { calculation = dummyIRS.calculation, common = dummyIRS.common, notary = DUMMY_NOTARY).apply { - setTime(TEST_TX_TIME, 30.seconds) + addTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) @@ -310,7 +309,7 @@ class IRSTests { val fixing = Fix(nextFix, "0.052".percent.value) InterestRateSwap().generateFix(tx, previousTXN.tx.outRef(0), fixing) with(tx) { - setTime(TEST_TX_TIME, 30.seconds) + addTimeWindow(TEST_TX_TIME, 30.seconds) signWith(MEGA_CORP_KEY) signWith(MINI_CORP_KEY) signWith(DUMMY_NOTARY_KEY) @@ -375,7 +374,7 @@ class IRSTests { transaction("Agreement") { output("irs post agreement") { singleIRS() } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } @@ -393,7 +392,7 @@ class IRSTests { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } } @@ -406,7 +405,7 @@ class IRSTests { input { irs } output("irs post agreement") { irs } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "There are no in states for an agreement" } } @@ -420,7 +419,7 @@ class IRSTests { irs.copy(calculation = irs.calculation.copy(fixedLegPaymentSchedule = emptySchedule)) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "There are events in the fix schedule" } } @@ -434,7 +433,7 @@ class IRSTests { irs.copy(calculation = irs.calculation.copy(floatingLegPaymentSchedule = emptySchedule)) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "There are events in the float schedule" } } @@ -447,7 +446,7 @@ class IRSTests { irs.copy(irs.fixedLeg.copy(notional = irs.fixedLeg.notional.copy(quantity = 0))) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "All notionals must be non zero" } @@ -456,7 +455,7 @@ class IRSTests { irs.copy(irs.fixedLeg.copy(notional = irs.floatingLeg.notional.copy(quantity = 0))) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "All notionals must be non zero" } } @@ -470,7 +469,7 @@ class IRSTests { modifiedIRS } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The fixed leg rate must be positive" } } @@ -487,7 +486,7 @@ class IRSTests { modifiedIRS } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The currency of the notionals must be the same" } } @@ -501,7 +500,7 @@ class IRSTests { modifiedIRS } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "All leg notionals must be the same" } } @@ -515,7 +514,7 @@ class IRSTests { modifiedIRS1 } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The effective date is before the termination date for the fixed leg" } @@ -525,7 +524,7 @@ class IRSTests { modifiedIRS2 } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The effective date is before the termination date for the floating leg" } } @@ -540,7 +539,7 @@ class IRSTests { modifiedIRS3 } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The termination dates are aligned" } @@ -551,7 +550,7 @@ class IRSTests { modifiedIRS4 } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this `fails with` "The effective dates are aligned" } } @@ -565,7 +564,7 @@ class IRSTests { transaction { output("irs post agreement") { singleIRS() } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } @@ -586,7 +585,7 @@ class IRSTests { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) output { newIRS } this.verifies() } @@ -594,7 +593,7 @@ class IRSTests { // This test makes sure that verify confirms the fixing was applied and there is a difference in the old and new tweak { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) output { oldIRS } this `fails with` "There is at least one difference in the IRS floating leg payment schedules" } @@ -602,7 +601,7 @@ class IRSTests { // This tests tries to sneak in a change to another fixing (which may or may not be the latest one) tweak { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) val firstResetKey = newIRS.calculation.floatingLegPaymentSchedule.keys.toList()[1] val firstResetValue = newIRS.calculation.floatingLegPaymentSchedule[firstResetKey] @@ -623,7 +622,7 @@ class IRSTests { // This tests modifies the payment currency for the fixing tweak { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld, Tenor("3M")), bd)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) val latestReset = newIRS.calculation.floatingLegPaymentSchedule.filter { it.value.rate is FixedRate }.maxBy { it.key } val modifiedLatestResetValue = latestReset!!.value.copy(notional = Amount(latestReset.value.notional.quantity, Currency.getInstance("JPY"))) @@ -666,7 +665,7 @@ class IRSTests { ) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } @@ -681,7 +680,7 @@ class IRSTests { ) } command(MEGA_CORP_PUBKEY) { InterestRateSwap.Commands.Agree() } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } @@ -710,7 +709,7 @@ class IRSTests { command(ORACLE_PUBKEY) { InterestRateSwap.Commands.Refix(Fix(FixOf("ICE LIBOR", ld1, Tenor("3M")), bd1)) } - timestamp(TEST_TX_TIME) + timeWindow(TEST_TX_TIME) this.verifies() } } diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt similarity index 71% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt rename to samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt index fd6d7c79cf..cce9a84d78 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/flows/UpdateBusinessDayFlow.kt @@ -1,41 +1,27 @@ package net.corda.irs.flows import co.paralleluniverse.fibers.Suspendable -import net.corda.core.identity.Party import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.StartableByRPC -import net.corda.core.node.CordaPluginRegistry +import net.corda.core.identity.Party import net.corda.core.node.NodeInfo -import net.corda.core.node.PluginServiceHub import net.corda.core.serialization.CordaSerializable import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.node.utilities.TestClock -import net.corda.testing.node.MockNetworkMapCache import java.time.LocalDate -import java.util.function.Function /** * This is a less temporary, demo-oriented way of initiating processing of temporal events. */ object UpdateBusinessDayFlow { - // This is not really a HandshakeMessage but needs to be so that the send uses the default session ID. This will - // resolve itself when the flow session stuff is done. @CordaSerializable data class UpdateBusinessDayMessage(val date: LocalDate) - class Plugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(::Service)) - } - - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(Broadcast::class.java, ::UpdateBusinessDayHandler) - } - } - + @InitiatedBy(Broadcast::class) private class UpdateBusinessDayHandler(val otherParty: Party) : FlowLogic() { override fun call() { val message = receive(otherParty).unwrap { it } @@ -43,7 +29,6 @@ object UpdateBusinessDayFlow { } } - @InitiatingFlow @StartableByRPC class Broadcast(val date: LocalDate, override val progressTracker: ProgressTracker) : FlowLogic() { @@ -66,7 +51,7 @@ object UpdateBusinessDayFlow { /** * Returns recipients ordered by legal name, with notary nodes taking priority over party nodes. * Ordering is required so that we avoid situations where on clock update a party starts a scheduled flow, but - * the notary or counterparty still use the old clock, so the timestamp on the transaction does not validate. + * the notary or counterparty still use the old clock, so the time-window on the transaction does not validate. */ private fun getRecipients(): Iterable { val notaryNodes = serviceHub.networkMapCache.notaryNodes @@ -76,12 +61,7 @@ object UpdateBusinessDayFlow { @Suspendable private fun doNextRecipient(recipient: NodeInfo) { - if (recipient.address is MockNetworkMapCache.MockAddress) { - // Ignore - } else { - send(recipient.legalIdentity, UpdateBusinessDayMessage(date)) - } + send(recipient.legalIdentity, UpdateBusinessDayMessage(date)) } } - } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/plugin/IRSDemoPlugin.kt b/samples/irs-demo/src/test/kotlin/net/corda/irs/plugin/IRSDemoPlugin.kt new file mode 100644 index 0000000000..c7d2e50474 --- /dev/null +++ b/samples/irs-demo/src/test/kotlin/net/corda/irs/plugin/IRSDemoPlugin.kt @@ -0,0 +1,9 @@ +package net.corda.irs.plugin + +import net.corda.irs.api.InterestRatesSwapDemoAPI +import net.corda.webserver.services.WebServerPluginRegistry +import java.util.function.Function + +class IRSDemoPlugin : WebServerPluginRegistry { + override val webApis = listOf(Function(::InterestRatesSwapDemoAPI)) +} \ No newline at end of file diff --git a/samples/trader-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry similarity index 58% rename from samples/trader-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry rename to samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry index 8ac62a0cd8..3f23b14e6a 100644 --- a/samples/trader-demo/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry +++ b/samples/irs-demo/src/test/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -1,2 +1,3 @@ # Register a ServiceLoader service extending from net.corda.core.node.CordaPluginRegistry -net.corda.traderdemo.plugin.TraderDemoPlugin +net.corda.irs.plugin.IRSPlugin +net.corda.irs.plugin.IRSDemoPlugin \ No newline at end of file diff --git a/samples/network-visualiser/build.gradle b/samples/network-visualiser/build.gradle index 7d95c04a9a..86337f2baa 100644 --- a/samples/network-visualiser/build.gradle +++ b/samples/network-visualiser/build.gradle @@ -7,16 +7,16 @@ apply plugin: 'us.kirchmeier.capsule' dependencies { compile project(':samples:irs-demo') + compile project(':test-utils') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" testCompile "junit:junit:$junit_version" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') - testCompile project(':test-utils') // Javax is required for webapis compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" 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 96d15ebb6e..655badd4c6 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 @@ -11,13 +11,13 @@ import javafx.scene.input.KeyCodeCombination import javafx.scene.layout.VBox import javafx.stage.Stage import javafx.util.Duration -import net.corda.core.messaging.SingleMessageRecipient +import net.corda.core.crypto.commonName 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.netmap.simulation.IRSSimulation +import net.corda.netmap.simulation.Simulation import net.corda.node.services.network.NetworkMapService import net.corda.node.services.statemachine.SessionConfirm import net.corda.node.services.statemachine.SessionEnd @@ -109,9 +109,9 @@ class NetworkMapVisualiser : Application() { } } // Fire the message bullets between nodes. - simulation.network.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer -> - val senderNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.sender) - val destNode: MockNetwork.MockNode = simulation.network.addressToNode(msg.recipients as SingleMessageRecipient) + simulation.mockNet.messagingNetwork.sentMessages.observeOn(uiThread).subscribe { msg: InMemoryMessagingNetwork.MessageTransfer -> + val senderNode: MockNetwork.MockNode = simulation.mockNet.addressToNode(msg.sender) + val destNode: MockNetwork.MockNode = simulation.mockNet.addressToNode(msg.recipients) if (transferIsInteresting(msg)) { viewModel.nodesToWidgets[senderNode]!!.pulseAnim.play() @@ -234,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}: $extraLabel" else node.info.legalIdentity.name.toString() + val label = if (extraLabel != null) "${node.info.legalIdentity.name.commonName}: $extraLabel" else node.info.legalIdentity.name.commonName val widget = view.buildProgressTrackerWidget(label, tracker.topLevelTracker) println("Added: $tracker, $widget") viewModel.trackerBoxes[tracker] = widget 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 d328a0fc74..afcfac1f43 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 @@ -7,8 +7,9 @@ import javafx.scene.layout.StackPane import javafx.scene.shape.Circle import javafx.scene.shape.Line import javafx.util.Duration +import net.corda.core.crypto.commonName import net.corda.core.utilities.ProgressTracker -import net.corda.irs.simulation.IRSSimulation +import net.corda.netmap.simulation.IRSSimulation import net.corda.testing.node.MockNetwork import org.bouncycastle.asn1.x500.X500Name import java.util.* @@ -49,7 +50,7 @@ class VisualiserViewModel { var bankCount: Int = 0 var serviceCount: Int = 0 - var stepDuration = Duration.millis(500.0) + var stepDuration: Duration = Duration.millis(500.0) var runningPausedState: NetworkMapVisualiser.RunningPausedState = NetworkMapVisualiser.RunningPausedState.Paused() var displayStyle: Style = Style.MAP @@ -155,7 +156,7 @@ class VisualiserViewModel { view.root.children += longPulseOuterDot view.root.children += innerDot - val nameLabel = Label(label.toString()) + val nameLabel = Label(label.commonName) val nameLabelRect = StackPane(nameLabel).apply { styleClass += "node-label" alignment = Pos.CENTER_RIGHT diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt similarity index 87% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt rename to samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt index 45b2f9a4d0..468c269b02 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/IRSSimulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/IRSSimulation.kt @@ -1,4 +1,4 @@ -package net.corda.irs.simulation +package net.corda.netmap.simulation import co.paralleluniverse.fibers.Suspendable import com.fasterxml.jackson.databind.ObjectMapper @@ -7,27 +7,27 @@ import com.google.common.util.concurrent.FutureCallback import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import net.corda.core.RunOnCallerThread +import net.corda.core.* import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.UniqueIdentifier -import net.corda.core.flatMap import net.corda.core.flows.FlowLogic -import net.corda.core.flows.FlowStateMachine +import net.corda.core.internal.FlowStateMachine +import net.corda.core.flows.InitiatedBy 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 import net.corda.core.transactions.SignedTransaction +import net.corda.core.utilities.DUMMY_CA import net.corda.flows.TwoPartyDealFlow.Acceptor import net.corda.flows.TwoPartyDealFlow.AutoOffer import net.corda.flows.TwoPartyDealFlow.Instigator import net.corda.irs.contract.InterestRateSwap +import net.corda.irs.flows.FixingFlow 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 rx.Observable import java.security.PublicKey import java.time.LocalDate import java.util.* @@ -47,7 +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 })) + om = JacksonSupport.createInMemoryMapper(InMemoryIdentityService((banks + regulators + networkMap).map { it.info.legalIdentityAndCert }, trustRoot = DUMMY_CA.certificate)) startIRSDealBetween(0, 1).success { // Next iteration is a pause. @@ -126,6 +126,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten irs.fixedLeg.fixedRatePayer = node1.info.legalIdentity irs.floatingLeg.floatingRatePayer = node2.info.legalIdentity + node1.registerInitiatedFlow(FixingFlow.Fixer::class.java) + node2.registerInitiatedFlow(FixingFlow.Fixer::class.java) + @InitiatingFlow class StartDealFlow(val otherParty: Party, val payload: AutoOffer, @@ -134,8 +137,13 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten override fun call(): SignedTransaction = subFlow(Instigator(otherParty, payload, myKey)) } + @InitiatedBy(StartDealFlow::class) + class AcceptDealFlow(otherParty: Party) : Acceptor(otherParty) + + val acceptDealFlows: Observable = node2.registerInitiatedFlow(AcceptDealFlow::class.java) + @Suppress("UNCHECKED_CAST") - val acceptorTx = node2.initiateSingleShotFlow(StartDealFlow::class) { Acceptor(it) }.flatMap { + val acceptorTxFuture = acceptDealFlows.toFuture().flatMap { (it.stateMachine as FlowStateMachine).resultFuture } @@ -146,9 +154,9 @@ class IRSSimulation(networkSendManuallyPumped: Boolean, runAsync: Boolean, laten node2.info.legalIdentity, AutoOffer(notary.info.notaryIdentity, irs), node1.services.legalIdentityKey) - val instigatorTx = node1.services.startFlow(instigator).resultFuture + val instigatorTxFuture = node1.services.startFlow(instigator).resultFuture - return Futures.allAsList(instigatorTx, acceptorTx).flatMap { instigatorTx } + return Futures.allAsList(instigatorTxFuture, acceptorTxFuture).flatMap { instigatorTxFuture } } override fun iterate(): InMemoryMessagingNetwork.MessageTransfer? { diff --git a/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt similarity index 90% rename from samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt rename to samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt index 6f770662c2..d3576d0cda 100644 --- a/samples/irs-demo/src/main/kotlin/net/corda/irs/simulation/Simulation.kt +++ b/samples/network-visualiser/src/main/kotlin/net/corda/netmap/simulation/Simulation.kt @@ -1,7 +1,8 @@ -package net.corda.irs.simulation +package net.corda.netmap.simulation import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import net.corda.core.crypto.locationOrNull import net.corda.core.flatMap import net.corda.core.flows.FlowLogic import net.corda.core.messaging.SingleMessageRecipient @@ -48,7 +49,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, throw IllegalArgumentException("The latency injector is only useful when using manual pumping.") } - val bankLocations = listOf(Pair("London", "UK"), Pair("Frankfurt", "DE"), Pair("Rome", "IT")) + val bankLocations = listOf(Pair("London", "GB"), Pair("Frankfurt", "DE"), Pair("Rome", "IT")) // This puts together a mock network of SimulatedNodes. @@ -56,7 +57,9 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger) : MockNetwork.MockNode(config, mockNet, networkMapAddress, advertisedServices, id, overrideServices, entropyRoot) { - override fun findMyLocation(): PhysicalLocation? = CityDatabase[configuration.nearestCity] + override fun findMyLocation(): PhysicalLocation? { + return configuration.myLegalName.locationOrNull?.let { CityDatabase[it] } + } } inner class BankFactory : MockNetwork.Factory { @@ -78,7 +81,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, fun createAll(): List { return bankLocations.mapIndexed { i, _ -> // Use deterministic seeds so the simulation is stable. Needed so that party owning keys are stable. - network.createNode(networkMap.info.address, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode + mockNet.createNode(networkMap.info.address, start = false, nodeFactory = this, entropyRoot = BigInteger.valueOf(i.toLong())) as SimulatedNode } } } @@ -118,7 +121,7 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, override fun create(config: NodeConfiguration, network: MockNetwork, networkMapAddr: SingleMessageRecipient?, advertisedServices: Set, id: Int, overrideServices: Map?, entropyRoot: BigInteger): MockNetwork.MockNode { - require(advertisedServices.containsType(NodeInterestRates.type)) + require(advertisedServices.containsType(NodeInterestRates.Oracle.type)) val cfg = TestNodeConfiguration( baseDirectory = config.baseDirectory, myLegalName = RATES_SERVICE_NAME, @@ -126,9 +129,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, return object : SimulatedNode(cfg, network, networkMapAddr, advertisedServices, id, overrideServices, entropyRoot) { override fun start(): MockNetwork.MockNode { super.start() + registerInitiatedFlow(NodeInterestRates.FixQueryHandler::class.java) + registerInitiatedFlow(NodeInterestRates.FixSignHandler::class.java) javaClass.classLoader.getResourceAsStream("net/corda/irs/simulation/example.rates.txt").use { database.transaction { - findService().upload(it) + installCordaService(NodeInterestRates.Oracle::class.java).uploadFixes(it.reader().readText()) } } return this @@ -153,15 +158,15 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } } - val network = MockNetwork(networkSendManuallyPumped, runAsync) + val mockNet = MockNetwork(networkSendManuallyPumped, runAsync) // This one must come first. val networkMap: SimulatedNode - = network.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) as SimulatedNode + = mockNet.createNode(null, nodeFactory = NetworkMapNodeFactory, advertisedServices = ServiceInfo(NetworkMapService.type)) as SimulatedNode val notary: SimulatedNode - = network.createNode(networkMap.info.address, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode - val regulators: List = listOf(network.createNode(networkMap.info.address, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) + = mockNet.createNode(networkMap.info.address, nodeFactory = NotaryNodeFactory, advertisedServices = ServiceInfo(SimpleNotaryService.type)) as SimulatedNode + val regulators: List = listOf(mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RegulatorFactory) as SimulatedNode) val ratesOracle: SimulatedNode - = network.createNode(networkMap.info.address, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.type)) as SimulatedNode + = mockNet.createNode(networkMap.info.address, start = false, nodeFactory = RatesOracleFactory, advertisedServices = ServiceInfo(NodeInterestRates.Oracle.type)) as SimulatedNode // All nodes must be in one of these two lists for the purposes of the visualiser tool. val serviceProviders: List = listOf(notary, ratesOracle, networkMap) @@ -215,11 +220,11 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, */ open fun iterate(): InMemoryMessagingNetwork.MessageTransfer? { if (networkSendManuallyPumped) { - network.messagingNetwork.pumpSend(false) + mockNet.messagingNetwork.pumpSend(false) } // Keep going until one of the nodes has something to do, or we have checked every node. - val endpoints = network.messagingNetwork.endpoints + val endpoints = mockNet.messagingNetwork.endpoints var countDown = endpoints.size while (countDown > 0) { val handledMessage = endpoints[pumpCursor].pumpReceive(false) @@ -266,10 +271,10 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } val networkInitialisationFinished: ListenableFuture<*> = - Futures.allAsList(network.nodes.map { it.networkMapRegistrationFuture }) + Futures.allAsList(mockNet.nodes.map { it.networkMapRegistrationFuture }) fun start(): ListenableFuture { - network.startNodes() + mockNet.startNodes() // Wait for all the nodes to have finished registering with the network map service. return networkInitialisationFinished.flatMap { startMainSimulation() } } @@ -283,6 +288,6 @@ abstract class Simulation(val networkSendManuallyPumped: Boolean, } fun stop() { - network.stopNodes() + mockNet.stopNodes() } } diff --git a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt similarity index 84% rename from samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt rename to samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt index bcaf971349..97777c9dbe 100644 --- a/samples/irs-demo/src/test/kotlin/net/corda/irs/testing/IRSSimulationTest.kt +++ b/samples/network-visualiser/src/test/kotlin/net/corda/netmap/simulation/IRSSimulationTest.kt @@ -1,8 +1,7 @@ -package net.corda.irs.testing +package net.corda.netmap.simulation import net.corda.core.getOrThrow import net.corda.core.utilities.LogHelper -import net.corda.irs.simulation.IRSSimulation import org.junit.Test class IRSSimulationTest { diff --git a/samples/notary-demo/build.gradle b/samples/notary-demo/build.gradle index 09842d2694..938b38cfd4 100644 --- a/samples/notary-demo/build.gradle +++ b/samples/notary-demo/build.gradle @@ -18,8 +18,8 @@ dependencies { testCompile "junit:junit:$junit_version" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':client:jfx') compile project(':client:rpc') @@ -49,15 +49,20 @@ publishing { } } +task deployNodes(dependsOn: ['deployNodesSingle', 'deployNodesRaft', 'deployNodesBFT']) + task deployNodesSingle(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesSingle" definitionClass = 'net.corda.notarydemo.SingleNotaryCordform' } task deployNodesRaft(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesRaft" definitionClass = 'net.corda.notarydemo.RaftNotaryCordform' } task deployNodesBFT(type: Cordform, dependsOn: 'jar') { + directory "./build/nodes/nodesBFT" definitionClass = 'net.corda.notarydemo.BFTNotaryCordform' } diff --git a/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt b/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt index 9f67b21a0e..2521dbee01 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/demorun/DemoRunner.kt @@ -1,10 +1,10 @@ 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 +import net.corda.testing.driver.NetworkMapStartStrategy +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver fun CordformDefinition.clean() { System.err.println("Deleting: $driverDirectory") diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt index 06a198c41d..4a0a5632f9 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/BFTNotaryCordform.kt @@ -12,13 +12,14 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.cordform.CordformDefinition import net.corda.cordform.CordformContext import net.corda.cordform.CordformNode -import net.corda.core.mapToArray +import net.corda.core.stream +import net.corda.core.toTypedArray import net.corda.node.services.transactions.minCorrectReplicas import org.bouncycastle.asn1.x500.X500Name fun main(args: Array) = BFTNotaryCordform.runNodes() -private val clusterSize = 4 // Minimum size thats tolerates a faulty replica. +private val clusterSize = 4 // Minimum size that tolerates a faulty replica. private val notaryNames = createNotaryNames(clusterSize) object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", notaryNames[0]) { @@ -37,7 +38,7 @@ object BFTNotaryCordform : CordformDefinition("build" / "notary-demo-nodes", not p2pPort(10005) rpcPort(10006) } - val clusterAddresses = (0 until clusterSize).mapToArray { HostAndPort.fromParts("localhost", 11000 + it * 10) } + val clusterAddresses = (0 until clusterSize).stream().mapToObj { HostAndPort.fromParts("localhost", 11000 + it * 10) }.toTypedArray() fun notaryNode(replicaId: Int, configure: CordformNode.() -> Unit) = node { name(notaryNames[replicaId]) advertisedServices(advertisedService) diff --git a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt index 8767dcb10a..0a3b416022 100644 --- a/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt +++ b/samples/notary-demo/src/main/kotlin/net/corda/notarydemo/Notarise.kt @@ -14,6 +14,7 @@ import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.BOB import net.corda.notarydemo.flows.DummyIssueAndMove import net.corda.notarydemo.flows.RPCStartableNotaryFlowClient +import kotlin.streams.asSequence fun main(args: Array) { val address = HostAndPort.fromParts("localhost", 10003) @@ -28,7 +29,8 @@ private class NotaryDemoClientApi(val rpc: CordaRPCOps) { private val notary by lazy { val (parties, partyUpdates) = rpc.networkMapUpdates() partyUpdates.notUsed() - parties.filter { it.advertisedServices.any { it.info.type.isNotary() } }.map { it.notaryIdentity }.distinct().single() + val id = parties.stream().filter { it.advertisedServices.any { it.info.type.isNotary() } }.map { it.notaryIdentity }.distinct().asSequence().singleOrNull() + checkNotNull(id) { "No unique notary identity, try cleaning the node directories." } } private val counterpartyNode by lazy { diff --git a/samples/simm-valuation-demo/build.gradle b/samples/simm-valuation-demo/build.gradle index 59bd02d389..f6c7a28aca 100644 --- a/samples/simm-valuation-demo/build.gradle +++ b/samples/simm-valuation-demo/build.gradle @@ -27,17 +27,13 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') - compile project(':node') compile project(':webserver') compile project(':finance') - testCompile project(':test-utils') // Javax is required for webapis compile "org.glassfish.jersey.core:jersey-server:${jersey_version}" @@ -54,21 +50,23 @@ dependencies { compile "com.opengamma.strata:strata-collect:${strata_version}" compile "com.opengamma.strata:strata-loader:${strata_version}" compile "com.opengamma.strata:strata-math:${strata_version}" + + testCompile project(':test-utils') + testCompile "junit:junit:$junit_version" + testCompile "org.assertj:assertj-core:${assertj_version}" } task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = [] } node { - name "CN=Bank A,O=Bank A,L=London,C=UK" - nearestCity "London" + name "CN=Bank A,O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10004 webPort 10005 @@ -76,7 +74,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "CN=Bank B,O=Bank B,L=New York,C=US" - nearestCity "New York" advertisedServices = [] p2pPort 10006 webPort 10007 @@ -84,7 +81,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "CN=Bank C,O=Bank C,L=Tokyo,C=Japan" - nearestCity "Tokyo" advertisedServices = [] p2pPort 10008 webPort 10009 diff --git a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt index cb62bc9bd7..171d373603 100644 --- a/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt +++ b/samples/simm-valuation-demo/src/integration-test/kotlin/net/corda/vega/SimmValuationTest.kt @@ -7,7 +7,7 @@ import net.corda.core.node.services.ServiceInfo 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.testing.driver.driver import net.corda.node.services.transactions.SimpleNotaryService import net.corda.testing.IntegrationTestCategory import net.corda.testing.http.HttpApi @@ -50,8 +50,9 @@ class SimmValuationTest : IntegrationTestCategory { } } - private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty = - getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty } + private fun getPartyWithName(partyApi: HttpApi, counterparty: X500Name): PortfolioApi.ApiParty { + return getAvailablePartiesFor(partyApi).counterparties.single { it.text == counterparty } + } private fun getAvailablePartiesFor(partyApi: HttpApi): PortfolioApi.AvailableParties { return partyApi.getJson("whoami") diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt index 58349e9309..9412b62262 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApi.kt @@ -2,10 +2,11 @@ package net.corda.vega.api import com.opengamma.strata.basics.currency.MultiCurrencyAmount import net.corda.client.rpc.notUsed -import net.corda.core.contracts.DealState +import net.corda.contracts.DealState import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.filterStatesOfType -import net.corda.core.crypto.* +import net.corda.core.crypto.parsePublicKeyBase58 +import net.corda.core.crypto.toBase58String import net.corda.core.getOrThrow import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party @@ -41,7 +42,7 @@ class PortfolioApi(val rpc: CordaRPCOps) { private inline fun dealsWith(party: AbstractParty): List> { val (vault, vaultUpdates) = rpc.vaultAndUpdates() vaultUpdates.notUsed() - return vault.filterStatesOfType().filter { it.state.data.parties.any { it == party } } + return vault.filterStatesOfType().filter { it.state.data.participants.any { it == party } } } /** @@ -153,7 +154,7 @@ class PortfolioApi(val rpc: CordaRPCOps) { return withParty(partyName) { val states = dealsWith(it) val tradeState = states.first { it.state.data.swap.id.second == tradeId }.state.data - Response.ok().entity(portfolioUtils.createTradeView(tradeState)).build() + Response.ok().entity(portfolioUtils.createTradeView(rpc, tradeState)).build() } } diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt index 82498b71ea..cda90b40d4 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt +++ b/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/api/PortfolioApiUtils.kt @@ -5,9 +5,12 @@ import com.opengamma.strata.product.swap.IborRateCalculation import com.opengamma.strata.product.swap.RateCalculationSwapLeg import com.opengamma.strata.product.swap.SwapLegType import net.corda.core.contracts.hash +import net.corda.core.crypto.commonName import net.corda.core.identity.AbstractParty import net.corda.core.identity.Party import net.corda.core.crypto.toBase58String +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.ServiceHub import net.corda.vega.contracts.IRSState import net.corda.vega.contracts.PortfolioState import net.corda.vega.portfolio.Portfolio @@ -123,16 +126,18 @@ class PortfolioApiUtils(private val ownParty: Party) { val common: Map, val ref: String) - fun createTradeView(state: IRSState): TradeView { + fun createTradeView(rpc: CordaRPCOps, state: IRSState): TradeView { val trade = if (state.buyer == ownParty as AbstractParty) state.swap.toFloatingLeg() else state.swap.toFloatingLeg() val fixedLeg = trade.product.legs.first { it.type == SwapLegType.FIXED } as RateCalculationSwapLeg val floatingLeg = trade.product.legs.first { it.type != SwapLegType.FIXED } as RateCalculationSwapLeg val fixedRate = fixedLeg.calculation as FixedRateCalculation val floatingRate = floatingLeg.calculation as IborRateCalculation + val fixedRatePayer: AbstractParty = rpc.partyFromKey(state.buyer.owningKey) ?: state.buyer + val floatingRatePayer: AbstractParty = rpc.partyFromKey(state.seller.owningKey) ?: state.seller return TradeView( fixedLeg = mapOf( - "fixedRatePayer" to state.buyer.owningKey.toBase58String(), + "fixedRatePayer" to (fixedRatePayer.nameOrNull()?.commonName ?: fixedRatePayer.owningKey.toBase58String()), "notional" to mapOf( "token" to fixedLeg.currency.code, "quantity" to fixedLeg.notionalSchedule.amount.initialValue @@ -148,7 +153,7 @@ class PortfolioApiUtils(private val ownParty: Party) { "paymentCalendar" to mapOf() // TODO ), floatingLeg = mapOf( - "floatingRatePayer" to state.seller.owningKey.toBase58String(), + "floatingRatePayer" to (floatingRatePayer.nameOrNull()?.commonName ?: floatingRatePayer.owningKey.toBase58String()), "notional" to mapOf( "token" to floatingLeg.currency.code, "quantity" to floatingLeg.notionalSchedule.amount.initialValue 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 35898c508d..1b61ac3eed 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 @@ -1,7 +1,7 @@ package net.corda.vega.contracts +import net.corda.contracts.DealState 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.crypto.keys @@ -21,17 +21,14 @@ data class IRSState(val swap: SwapData, 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 participants: List get() = listOf(buyer, seller) override fun isRelevant(ourKeys: Set): Boolean { - return parties.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() + return participants.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() } override fun generateAgreement(notary: Party): TransactionBuilder { val state = IRSState(swap, buyer, seller, OGTrade()) - return TransactionType.General.Builder(notary).withItems(state, Command(OGTrade.Commands.Agree(), parties.map { it.owningKey })) + return TransactionType.General.Builder(notary).withItems(state, Command(OGTrade.Commands.Agree(), participants.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 97cc7dc43b..488a9dd4a1 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 @@ -9,20 +9,20 @@ import java.math.BigDecimal * Specifies the contract between two parties that trade an OpenGamma IRS. Currently can only agree to trade. */ data class OGTrade(override val legalContractReference: SecureHash = SecureHash.sha256("OGTRADE.KT")) : Contract { - override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) + override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select()) interface Commands : CommandData { class Agree : TypeOnlyCommandData(), Commands // Both sides agree to trade } interface Clauses { - class Timestamped : Clause() { + class TimeWindowed : Clause() { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: Unit?): Set { - require(tx.timestamp?.midpoint != null) { "must be timestamped" } + require(tx.timeWindow?.midpoint != null) { "must have a time-window" } // We return an empty set because we don't process any commands return emptySet() } @@ -45,8 +45,8 @@ 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.containsAll(outputs[0].participants)) - require(outputs[0].parties.containsAll(listOf(outputs[0].buyer, outputs[0].seller))) + require(outputs[0].participants.containsAll(outputs[0].participants)) + require(outputs[0].participants.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)) require(outputs[0].swap.tradeDate.isBefore(outputs[0].swap.endDate)) 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 083fe655bd..6fcbb6eba3 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,5 +1,6 @@ package net.corda.vega.contracts +import net.corda.contracts.DealState import net.corda.core.contracts.* import net.corda.core.crypto.keys import net.corda.core.flows.FlowLogicRefFactory @@ -27,12 +28,9 @@ 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 participants: List get() = _parties.toList() override val ref: String = linearId.toString() - val valuer: AbstractParty get() = parties[0] - - override val participants: List - get() = parties + val valuer: AbstractParty get() = participants[0] override fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity { val flow = flowLogicRefFactory.create(SimmRevaluation.Initiator::class.java, thisStateRef, LocalDate.now()) @@ -40,11 +38,11 @@ data class PortfolioState(val portfolio: List, } override fun isRelevant(ourKeys: Set): Boolean { - return parties.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() + return participants.flatMap { it.owningKey.keys }.intersect(ourKeys).isNotEmpty() } override fun generateAgreement(notary: Party): TransactionBuilder { - return TransactionType.General.Builder(notary).withItems(copy(), Command(PortfolioSwap.Commands.Agree(), parties.map { it.owningKey })) + return TransactionType.General.Builder(notary).withItems(copy(), Command(PortfolioSwap.Commands.Agree(), participants.map { it.owningKey })) } override fun generateRevision(notary: Party, oldState: StateAndRef<*>, updatedValue: Update): TransactionBuilder { @@ -55,7 +53,7 @@ data class PortfolioState(val portfolio: List, val tx = TransactionType.General.Builder(notary) tx.addInputState(oldState) tx.addOutputState(copy(portfolio = portfolio, valuation = valuation)) - tx.addCommand(PortfolioSwap.Commands.Update(), parties.map { it.owningKey }) + tx.addCommand(PortfolioSwap.Commands.Update(), participants.map { it.owningKey }) return tx } } 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 4dad44ad41..c946500748 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 @@ -10,7 +10,7 @@ import net.corda.core.crypto.SecureHash * of the portfolio arbitrarily. */ data class PortfolioSwap(override val legalContractReference: SecureHash = SecureHash.sha256("swordfish")) : Contract { - override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.Timestamped(), Clauses.Group()), tx.commands.select()) + override fun verify(tx: TransactionForContract) = verifyClause(tx, AllOf(Clauses.TimeWindowed(), Clauses.Group()), tx.commands.select()) interface Commands : CommandData { class Agree : TypeOnlyCommandData(), Commands // Both sides agree to portfolio @@ -18,13 +18,13 @@ data class PortfolioSwap(override val legalContractReference: SecureHash = Secur } interface Clauses { - class Timestamped : Clause() { + class TimeWindowed : Clause() { override fun verify(tx: TransactionForContract, inputs: List, outputs: List, commands: List>, groupingKey: Unit?): Set { - require(tx.timestamp?.midpoint != null) { "must be timestamped" } + require(tx.timeWindow?.midpoint != null) { "must have a time-window)" } // We return an empty set because we don't process any commands return emptySet() } @@ -70,8 +70,7 @@ data class PortfolioSwap(override val legalContractReference: SecureHash = Secur requireThat { "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.containsAll(outputs[0].participants)) + "valuer must be a party" using (outputs[0].participants.contains(outputs[0].valuer)) } 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 11a8363f92..ec39e261a0 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 @@ -2,10 +2,10 @@ package net.corda.vega.flows import co.paralleluniverse.fibers.Suspendable import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy 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 import net.corda.core.utilities.unwrap @@ -15,12 +15,6 @@ import net.corda.vega.contracts.OGTrade import net.corda.vega.contracts.SwapData object IRSTradeFlow { - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(Requester::class.java, ::Receiver) - } - } - @CordaSerializable data class OfferMessage(val notary: Party, val dealBeingOffered: IRSState) @@ -52,6 +46,7 @@ object IRSTradeFlow { } + @InitiatedBy(Requester::class) class Receiver(private val replyToParty: Party) : FlowLogic() { @Suspendable override fun call() { 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 94061ba0d5..9553a5347e 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 @@ -8,22 +8,20 @@ import com.opengamma.strata.pricer.curve.CalibrationMeasures import com.opengamma.strata.pricer.curve.CurveCalibrator import com.opengamma.strata.pricer.rate.ImmutableRatesProvider import com.opengamma.strata.pricer.swap.DiscountingSwapProductPricer +import net.corda.contracts.dealsWith import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateRef import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy 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 import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.unwrap import net.corda.flows.AbstractStateReplacementFlow.Proposal import net.corda.flows.StateReplacementException import net.corda.flows.TwoPartyDealFlow -import net.corda.node.services.messaging.Ack import net.corda.vega.analytics.* import net.corda.vega.contracts.* import net.corda.vega.portfolio.Portfolio @@ -181,18 +179,10 @@ object SimmFlow { } } - /** - * Service plugin for listening for incoming Simm flow communication - */ - class Service(services: PluginServiceHub) { - init { - services.registerServiceFlow(Requester::class.java, ::Receiver) - } - } - /** * Receives and validates a portfolio and comes to consensus over the portfolio initial margin using SIMM. */ + @InitiatedBy(Requester::class) class Receiver(val replyToParty: Party) : FlowLogic() { lateinit var ownParty: Party lateinit var offer: OfferMessage @@ -329,4 +319,6 @@ object SimmFlow { }) } } + + private object Ack } 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 e687fd710f..da189a9204 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 @@ -22,8 +22,8 @@ object SimmRevaluation { val stateAndRef = serviceHub.vaultService.linearHeadsOfType().values.first { it.ref == curStateRef } val curState = stateAndRef.state.data val myIdentity = serviceHub.myInfo.legalIdentity - if (myIdentity == curState.parties[0]) { - val otherParty = serviceHub.identityService.partyFromAnonymous(curState.parties[1]) + if (myIdentity == curState.participants[0]) { + val otherParty = serviceHub.identityService.partyFromAnonymous(curState.participants[1]) require(otherParty != null) { "Other party must be known by this node" } subFlow(SimmFlow.Requester(otherParty!!, valuationDate, stateAndRef)) } 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 d31711549b..889a7071b1 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 @@ -8,7 +8,6 @@ import net.corda.core.transactions.SignedTransaction import net.corda.flows.AbstractStateReplacementFlow import net.corda.flows.StateReplacementException import net.corda.vega.contracts.RevisionedState -import java.security.PublicKey /** * Flow that generates an update on a mutable deal state and commits the resulting transaction reaching consensus @@ -20,7 +19,7 @@ object StateRevisionFlow { 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.addTimeWindow(serviceHub.clock.instant(), 30.seconds) 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 06d14023d7..dd318fc617 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,26 +10,24 @@ 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.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.flows.IRSTradeFlow -import net.corda.vega.flows.SimmFlow +import net.corda.webserver.services.WebServerPluginRegistry import java.util.function.Function /** - * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow - * It also enables a human usable web service for demo purposes - if available. - * It is loaded via discovery - see [CordaPluginRegistry] + * [SimmService] is the object that makes available the flows and services for the Simm agreement / evaluation flow. + * It is loaded via discovery - see [CordaPluginRegistry]. + * It is also the object that enables a human usable web service for demo purpose + * It is loaded via discovery see [WebServerPluginRegistry]. */ object SimmService { - class Plugin : CordaPluginRegistry() { + class Plugin : CordaPluginRegistry(), WebServerPluginRegistry { override val webApis = listOf(Function(::PortfolioApi)) 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 { custom.apply { // OpenGamma classes. diff --git a/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry new file mode 100644 index 0000000000..7fabecaa0c --- /dev/null +++ b/samples/simm-valuation-demo/src/main/resources/META-INF/services/net.corda.webserver.services.WebServerPluginRegistry @@ -0,0 +1,2 @@ +# Register a ServiceLoader service extending from net.corda.webserver.services.WebServerPluginRegistry +net.corda.vega.services.SimmService$Plugin \ No newline at end of file diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js index 9aaee0cd55..53138a1896 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/app.component.js @@ -24,11 +24,26 @@ var AppComponent = (function () { AppComponent.prototype.refreshValue = function (value) { this.counterparty = this.httpWrapperService.setCounterparty(value.id); }; + AppComponent.prototype.renderX500Name = function (x500Name) { + var name = x500Name + x500Name.split(',').forEach(function(element) { + var keyValue = element.split('='); + if (keyValue[0].toUpperCase() == 'CN') { + name = keyValue[1]; + } + }); + return name; + }; AppComponent.prototype.ngOnInit = function () { var _this = this; this.httpWrapperService.getAbsolute("whoami").toPromise().then(function (data) { - _this.whoAmI = data.self.text; - _this.counterParties = data.counterparties; + _this.whoAmI = _this.renderX500Name(data.self.text); + _this.counterParties = data.counterparties.map(function(x) { + return { + id: x.id, + text: _this.renderX500Name(x.text) + }; + }); if (_this.counterParties.length == 0) { console.log("/whoami is returning no counterparties, the whole app won't run", data); } diff --git a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js index f429b39945..a491a2a5a1 100644 --- a/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js +++ b/samples/simm-valuation-demo/src/main/resources/simmvaluationweb/app/viewmodel/FixedLegViewModel.js @@ -1,7 +1,7 @@ "use strict"; var FixedLegViewModel = (function () { function FixedLegViewModel() { - this.fixedRatePayer = "CN=Bank A,O=Bank A,L=London,C=UK"; + this.fixedRatePayer = "CN=Bank A,O=Bank A,L=London,C=GB"; this.notional = { quantity: 2500000000 }; diff --git a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/Main.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt similarity index 84% rename from samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/Main.kt rename to samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt index b774b782d4..3410f4ea48 100644 --- a/samples/simm-valuation-demo/src/main/kotlin/net/corda/vega/Main.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/Main.kt @@ -1,14 +1,14 @@ package net.corda.vega import com.google.common.util.concurrent.Futures -import net.corda.core.crypto.X509Utilities import net.corda.core.getOrThrow import net.corda.core.node.services.ServiceInfo import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_BANK_B import net.corda.core.utilities.DUMMY_BANK_C -import net.corda.node.driver.driver +import net.corda.core.utilities.DUMMY_NOTARY import net.corda.node.services.transactions.SimpleNotaryService +import net.corda.testing.driver.driver /** * Sample main used for running within an IDE. Starts 4 nodes (A, B, C and Notary/Controller) as an alternative to running via gradle @@ -17,7 +17,7 @@ import net.corda.node.services.transactions.SimpleNotaryService */ fun main(args: Array) { driver(dsl = { - startNode(X509Utilities.getDevX509Name("Controller"), setOf(ServiceInfo(SimpleNotaryService.type))) + startNode(DUMMY_NOTARY.name, setOf(ServiceInfo(SimpleNotaryService.type))) val (nodeA, nodeB, nodeC) = Futures.allAsList( startNode(DUMMY_BANK_A.name), startNode(DUMMY_BANK_B.name), diff --git a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/SwapExample.kt b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/SwapExample.kt index 1afb906701..96c22c135f 100644 --- a/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/SwapExample.kt +++ b/samples/simm-valuation-demo/src/test/kotlin/net/corda/vega/SwapExample.kt @@ -52,7 +52,7 @@ fun main(args: Array) { class SwapExample { - val VALUATION_DATE = LocalDate.of(2016, 6, 6) + val VALUATION_DATE = LocalDate.of(2016, 6, 6)!! fun main(@Suppress("UNUSED_PARAMETER") args: Array) { val curveGroupDefinition = loadCurveGroup() diff --git a/samples/trader-demo/build.gradle b/samples/trader-demo/build.gradle index 0d02f23f78..e21103cd7a 100644 --- a/samples/trader-demo/build.gradle +++ b/samples/trader-demo/build.gradle @@ -23,15 +23,12 @@ configurations { dependencies { compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" - testCompile "junit:junit:$junit_version" - testCompile "org.assertj:assertj-core:${assertj_version}" // Corda integration dependencies - runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts') - runtime project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') + compile project(path: ":node:capsule", configuration: 'runtimeArtifacts') + compile project(path: ":webserver:webcapsule", configuration: 'runtimeArtifacts') compile project(':core') compile project(':finance') - compile project(':test-utils') // Corda Plugins: dependent flows and services compile project(':samples:bank-of-corda-demo') @@ -45,6 +42,10 @@ dependencies { exclude group: "bouncycastle" } + testCompile project(':test-utils') + testCompile "junit:junit:$junit_version" + testCompile "org.assertj:assertj-core:${assertj_version}" + // Cordapp dependencies // Specify your cordapp's dependencies below, including dependent cordapps } @@ -58,17 +59,15 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { directory "./build/nodes" // This name "Notary" is hard-coded into TraderDemoClientApi so if you change it here, change it there too. // In this demo the node that runs a standalone notary also acts as the network map server. - networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" + networkMap "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" node { - name "CN=Notary Service,O=R3,OU=corda,L=London,C=UK" - nearestCity "London" + name "CN=Notary Service,O=R3,OU=corda,L=London,C=GB" advertisedServices = ["corda.notary.validating"] p2pPort 10002 cordapps = [] } node { - name "CN=Bank A,O=Bank A,L=London,C=UK" - nearestCity "London" + name "CN=Bank A,O=Bank A,L=London,C=GB" advertisedServices = [] p2pPort 10005 rpcPort 10006 @@ -77,7 +76,6 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { } node { name "CN=Bank B,O=Bank B,L=New York,C=US" - nearestCity "New York" advertisedServices = [] p2pPort 10008 rpcPort 10009 @@ -85,8 +83,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) { rpcUsers = ext.rpcUsers } node { - name "CN=BankOfCorda,O=R3,OU=corda,L=New York,C=US" - nearestCity "London" + name "CN=BankOfCorda,O=R3,L=New York,C=US" advertisedServices = [] p2pPort 10011 cordapps = [] diff --git a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt index fb73735ec3..cc575dd8b1 100644 --- a/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt +++ b/samples/trader-demo/src/integration-test/kotlin/net/corda/traderdemo/TraderDemoTest.kt @@ -10,12 +10,13 @@ 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.flows.IssuerFlow -import net.corda.node.driver.poll +import net.corda.testing.driver.poll import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User import net.corda.testing.BOC import net.corda.testing.node.NodeBasedTest +import net.corda.traderdemo.flow.BuyerFlow import net.corda.traderdemo.flow.SellerFlow import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -36,6 +37,8 @@ class TraderDemoTest : NodeBasedTest() { startNode(DUMMY_NOTARY.name, advertisedServices = setOf(ServiceInfo(SimpleNotaryService.type))) ).getOrThrow() + nodeA.registerInitiatedFlow(BuyerFlow::class.java) + val (nodeARpc, nodeBRpc) = listOf(nodeA, nodeB).map { val client = CordaRPCClient(it.configuration.rpcAddress!!) client.start(demoUser[0].username, demoUser[0].password).proxy @@ -57,12 +60,8 @@ class TraderDemoTest : NodeBasedTest() { val executor = Executors.newScheduledThreadPool(1) poll(executor, "A to be notified of the commercial paper", pollInterval = 100.millis) { val actualPaper = listOf(clientA.commercialPaperCount, clientB.commercialPaperCount) - if (actualPaper == expectedPaper) { - Unit - } else { - null - } - }.get() + if (actualPaper == expectedPaper) Unit else null + }.getOrThrow() executor.shutdown() assertThat(clientA.dollarCashBalance).isEqualTo(95.DOLLARS) assertThat(clientB.dollarCashBalance).isEqualTo(5.DOLLARS) diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt index 6b445e2b69..aca2ca11da 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/TraderDemoClientApi.kt @@ -73,6 +73,6 @@ class TraderDemoClientApi(val rpc: CordaRPCOps) { // The line below blocks and waits for the future to resolve. val stx = rpc.startFlow(::SellerFlow, otherParty, amount).returnValue.getOrThrow() - logger.info("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}") + println("Sale completed - we have a happy customer!\n\nFinal transaction is:\n\n${Emoji.renderIfSupported(stx.tx)}") } } diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt index 6d01fdd3a7..53024ad9a5 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt +++ b/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/flow/BuyerFlow.kt @@ -4,36 +4,24 @@ import co.paralleluniverse.fibers.Suspendable import net.corda.contracts.CommercialPaper import net.corda.core.contracts.Amount import net.corda.core.contracts.TransactionGraphSearch -import net.corda.core.identity.Party +import net.corda.core.div import net.corda.core.flows.FlowLogic +import net.corda.core.flows.InitiatedBy +import net.corda.core.identity.Party import net.corda.core.node.NodeInfo -import net.corda.core.node.PluginServiceHub -import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction import net.corda.core.utilities.Emoji import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.unwrap import net.corda.flows.TwoPartyTradeFlow -import java.nio.file.Paths import java.util.* -class BuyerFlow(val otherParty: Party, - private val attachmentsDirectory: String, - override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY)) : FlowLogic() { +@InitiatedBy(SellerFlow::class) +class BuyerFlow(val otherParty: Party) : FlowLogic() { object STARTING_BUY : ProgressTracker.Step("Seller connected, purchasing commercial paper asset") - class Service(services: PluginServiceHub) : SingletonSerializeAsToken() { - init { - // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. - // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. - val attachmentsPath = (services.storageService.attachments).let { - it.automaticallyExtractAttachments = true - it.storePath - } - services.registerServiceFlow(SellerFlow::class.java) { BuyerFlow(it, attachmentsPath.toString()) } - } - } + override val progressTracker: ProgressTracker = ProgressTracker(STARTING_BUY) @Suspendable override fun call() { @@ -72,8 +60,15 @@ class BuyerFlow(val otherParty: Party, followInputsOfType = CommercialPaper.State::class.java) val cpIssuance = search.call().single() + // Buyer will fetch the attachment from the seller automatically when it resolves the transaction. + // For demo purposes just extract attachment jars when saved to disk, so the user can explore them. + val attachmentsPath = (serviceHub.storageService.attachments).let { + it.automaticallyExtractAttachments = true + it.storePath + } + cpIssuance.attachments.first().let { - val p = Paths.get(attachmentsDirectory, "$it.jar") + val p = attachmentsPath / "$it.jar" println(""" The issuance of the commercial paper came with an attachment. You can find it expanded in this directory: 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 8a6239684b..3a7ecba1d7 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 @@ -10,8 +10,8 @@ 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.identity.Party import net.corda.core.node.NodeInfo import net.corda.core.seconds import net.corda.core.transactions.SignedTransaction @@ -19,7 +19,6 @@ import net.corda.core.utilities.ProgressTracker import net.corda.flows.NotaryFlow import net.corda.flows.TwoPartyTradeFlow import net.corda.testing.BOC -import java.security.PublicKey import java.time.Instant import java.util.* @@ -82,13 +81,13 @@ class SellerFlow(val otherParty: Party, // Attach the prospectus. tx.addAttachment(serviceHub.storageService.attachments.openAttachment(PROSPECTUS_HASH)!!.id) - // Requesting timestamping, all CP must be timestamped. - tx.setTime(Instant.now(), 30.seconds) + // Requesting a time-window to be set, all CP must have a validation window. + tx.addTimeWindow(Instant.now(), 30.seconds) // Sign it as ourselves. tx.signWith(keyPair) - // Get the notary to sign the timestamp + // Get the notary to sign the time-window. val notarySigs = subFlow(NotaryFlow.Client(tx.toSignedTransaction(false))) notarySigs.forEach { tx.addSignatureUnchecked(it) } 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 deleted file mode 100644 index cd45ed4b95..0000000000 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/plugin/TraderDemoPlugin.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.traderdemo.plugin - -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry -import net.corda.traderdemo.flow.BuyerFlow -import java.util.function.Function - -class TraderDemoPlugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(BuyerFlow::Service)) -} diff --git a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt similarity index 90% rename from samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt rename to samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt index 1660ae7bfa..af4638e205 100644 --- a/samples/trader-demo/src/main/kotlin/net/corda/traderdemo/Main.kt +++ b/samples/trader-demo/src/test/kotlin/net/corda/traderdemo/Main.kt @@ -6,11 +6,12 @@ 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.flows.IssuerFlow -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 net.corda.testing.BOC +import net.corda.testing.driver.driver +import net.corda.traderdemo.flow.SellerFlow /** * This file is exclusively for being able to run your nodes through an IDE (as opposed to running deployNodes) @@ -19,7 +20,7 @@ import net.corda.testing.BOC fun main(args: Array) { val permissions = setOf( startFlowPermission(), - startFlowPermission()) + startFlowPermission()) val demoUser = listOf(User("demo", "demo", permissions)) driver(driverDirectory = "build" / "trader-demo-nodes", isDebug = true) { val user = User("user1", "test", permissions = setOf(startFlowPermission())) diff --git a/settings.gradle b/settings.gradle index a97cca92ad..aad7b883c0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -21,6 +21,7 @@ include 'experimental:quasar-hook' include 'experimental:intellij-plugin' include 'verifier' include 'test-utils' +include 'smoke-test-utils' include 'tools:explorer' include 'tools:explorer:capsule' include 'tools:demobench' diff --git a/smoke-test-utils/build.gradle b/smoke-test-utils/build.gradle new file mode 100644 index 0000000000..dcd94fae66 --- /dev/null +++ b/smoke-test-utils/build.gradle @@ -0,0 +1,8 @@ +apply plugin: 'kotlin' + +description 'Utilities needed for smoke tests in Corda' + +dependencies { + // Smoke tests do NOT have any Node code on the classpath! + compile project(':client:rpc') +} diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt similarity index 79% rename from client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt rename to smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt index 75c4074be1..338c88d656 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeConfig.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeConfig.kt @@ -1,6 +1,10 @@ -package net.corda.kotlin.rpc +package net.corda.smoketesting -import com.typesafe.config.* +import com.typesafe.config.Config +import com.typesafe.config.ConfigFactory.empty +import com.typesafe.config.ConfigRenderOptions +import com.typesafe.config.ConfigValue +import com.typesafe.config.ConfigValueFactory import net.corda.core.crypto.commonName import net.corda.core.identity.Party import net.corda.nodeapi.User @@ -18,13 +22,13 @@ class NodeConfig( val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false) } - val commonName: String = party.name.commonName + val commonName: String get() = party.name.commonName /* * The configuration object depends upon the networkMap, * which is mutable. */ - fun toFileConfig(): Config = ConfigFactory.empty() + fun toFileConfig(): Config = empty() .withValue("myLegalName", valueFor(party.name.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) @@ -42,7 +46,6 @@ class NodeConfig( 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) + return if (obj == null) empty() else body(empty(), obj).atPath(path) } } diff --git a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt similarity index 73% rename from client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt rename to smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt index 3d81f9ad83..c939bd4758 100644 --- a/client/rpc/src/smoke-test/kotlin/net/corda/kotlin/rpc/NodeProcess.kt +++ b/smoke-test-utils/src/main/kotlin/net/corda/smoketesting/NodeProcess.kt @@ -1,16 +1,18 @@ -package net.corda.kotlin.rpc +package net.corda.smoketesting import com.google.common.net.HostAndPort import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCConnection +import net.corda.core.createDirectories +import net.corda.core.div 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.time.Instant +import java.time.ZoneId.systemDefault +import java.time.format.DateTimeFormatter import java.util.concurrent.Executors import java.util.concurrent.TimeUnit.SECONDS -import kotlin.test.* class NodeProcess( val config: NodeConfig, @@ -21,9 +23,7 @@ class NodeProcess( 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") + val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").withZone(systemDefault()) } fun connect(): CordaRPCConnection { @@ -40,16 +40,20 @@ class NodeProcess( } log.info("Deleting Artemis directories, because they're large!") - nodeDir.resolve("artemis").toFile().deleteRecursively() + (nodeDir / "artemis").toFile().deleteRecursively() } - class Factory(val nodesDir: Path) { + class Factory(val buildDirectory: Path = Paths.get("build"), + val cordaJar: Path = Paths.get(this::class.java.getResource("/corda.jar").toURI())) { + val nodesDirectory = buildDirectory / formatter.format(Instant.now()) init { - assertTrue(nodesDir.toFile().forceDirectory(), "Directory '$nodesDir' does not exist") + nodesDirectory.createDirectories() } + fun baseDirectory(config: NodeConfig): Path = nodesDirectory / config.commonName + fun create(config: NodeConfig): NodeProcess { - val nodeDir = Files.createTempDirectory(nodesDir, config.commonName) + val nodeDir = baseDirectory(config).createDirectories() log.info("Node directory: {}", nodeDir) val confFile = nodeDir.resolve("node.conf").toFile() @@ -78,7 +82,7 @@ class NodeProcess( }, 5, 1, SECONDS) val setupOK = setupExecutor.awaitTermination(120, SECONDS) - assertTrue(setupOK && process.isAlive, "Failed to create RPC connection") + check(setupOK && process.isAlive) { "Failed to create RPC connection" } } catch (e: Exception) { process.destroyForcibly() throw e @@ -91,17 +95,14 @@ class NodeProcess( private fun startNode(nodeDir: Path): Process { val builder = ProcessBuilder() - .command(javaPath.toString(), "-jar", corda.path) + .command(javaPath.toString(), "-jar", cordaJar.toString()) .directory(nodeDir.toFile()) builder.environment().putAll(mapOf( - "CAPSULE_CACHE_DIR" to capsuleDir.toString() + "CAPSULE_CACHE_DIR" to (buildDirectory / "capsule").toString() )) return builder.start() } } } - -private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs() - diff --git a/test-utils/build.gradle b/test-utils/build.gradle index 8209fea019..3c5e40f6fe 100644 --- a/test-utils/build.gradle +++ b/test-utils/build.gradle @@ -8,6 +8,22 @@ description 'Testing utilities for Corda' configurations { // we don't want isolated.jar in classPath, since we want to test jar being dynamically loaded as an attachment runtime.exclude module: 'isolated' + + integrationTestCompile.extendsFrom testCompile + integrationTestRuntime.extendsFrom testRuntime +} + +sourceSets { + integrationTest { + kotlin { + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + srcDir file('src/integration-test/kotlin') + } + resources { + srcDir file('src/integration-test/resources') + } + } } dependencies { @@ -15,7 +31,6 @@ dependencies { compile project(':core') compile project(':node') compile project(':webserver') - compile project(':verifier') compile project(':client:mock') compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version" @@ -33,4 +48,21 @@ dependencies { // OkHTTP: Simple HTTP library. compile "com.squareup.okhttp3:okhttp:$okhttp_version" + + // Integration test helpers + integrationTestCompile "org.assertj:assertj-core:${assertj_version}" + integrationTestCompile "junit:junit:$junit_version" +} + +task integrationTest(type: Test) { + testClassesDir = sourceSets.integrationTest.output.classesDir + classpath = sourceSets.integrationTest.runtimeClasspath +} + +jar { + baseName 'corda-test-utils' +} + +publish { + name = jar.baseName } diff --git a/node/src/integration-test/kotlin/net/corda/node/driver/DriverTests.kt b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt similarity index 89% rename from node/src/integration-test/kotlin/net/corda/node/driver/DriverTests.kt rename to test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt index cd39098c83..40079f3bdf 100644 --- a/node/src/integration-test/kotlin/net/corda/node/driver/DriverTests.kt +++ b/test-utils/src/integration-test/kotlin/net/corda/testing/driver/DriverTests.kt @@ -1,4 +1,4 @@ -package net.corda.node.driver +package net.corda.testing.driver import com.google.common.util.concurrent.ListenableFuture import net.corda.core.div @@ -9,7 +9,7 @@ import net.corda.core.readLines import net.corda.core.utilities.DUMMY_BANK_A import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_REGULATOR -import net.corda.node.LOGS_DIRECTORY_NAME +import net.corda.node.internal.NodeStartup import net.corda.node.services.api.RegulatorService import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.ArtemisMessagingComponent @@ -25,10 +25,10 @@ class DriverTests { private val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) - private fun nodeMustBeUp(handleFuture: ListenableFuture) = handleFuture.getOrThrow().apply { + private fun nodeMustBeUp(handleFuture: ListenableFuture) = handleFuture.getOrThrow().apply { val hostAndPort = ArtemisMessagingComponent.toHostAndPort(nodeInfo.address) // Check that the port is bound - addressMustBeBound(executorService, hostAndPort, process) + addressMustBeBound(executorService, hostAndPort, (this as? NodeHandle.OutOfProcess)?.process) } private fun nodeMustBeDown(handle: NodeHandle) { @@ -74,7 +74,7 @@ class DriverTests { assertThat(logConfigFile).isRegularFile() driver(isDebug = true, systemProperties = mapOf("log4j.configurationFile" to logConfigFile.toString())) { val baseDirectory = startNode(DUMMY_BANK_A.name).getOrThrow().configuration.baseDirectory - val logFile = (baseDirectory / LOGS_DIRECTORY_NAME).list { it.sorted().findFirst().get() } + val logFile = (baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME).list { it.sorted().findFirst().get() } val debugLinesPresent = logFile.readLines { lines -> lines.anyMatch { line -> line.startsWith("[DEBUG]") } } assertThat(debugLinesPresent).isTrue() } 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 895cc0024d..6ac2399c8e 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/CoreTestUtils.kt @@ -4,24 +4,23 @@ package net.corda.testing import com.google.common.net.HostAndPort -import com.google.common.util.concurrent.ListenableFuture import net.corda.core.contracts.StateRef -import net.corda.core.crypto.* -import net.corda.core.flows.FlowLogic +import net.corda.core.crypto.SecureHash +import net.corda.core.crypto.X509Utilities +import net.corda.core.crypto.commonName +import net.corda.core.crypto.generateKeyPair +import net.corda.core.identity.AnonymousParty import net.corda.core.identity.Party +import net.corda.core.identity.PartyAndCertificate 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 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.MockServices @@ -34,9 +33,9 @@ import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair import java.security.PublicKey +import java.security.cert.CertPath import java.util.* import java.util.concurrent.atomic.AtomicInteger -import kotlin.reflect.KClass /** * JAVA INTEROP @@ -70,22 +69,27 @@ val ALICE_PUBKEY: PublicKey get() = ALICE_KEY.public val BOB_PUBKEY: PublicKey get() = BOB_KEY.public val CHARLIE_PUBKEY: PublicKey get() = CHARLIE_KEY.public -val MEGA_CORP: Party get() = Party(X509Utilities.getDevX509Name("MegaCorp"), MEGA_CORP_PUBKEY) -val MINI_CORP: Party get() = Party(X509Utilities.getDevX509Name("MiniCorp"), MINI_CORP_PUBKEY) +val MEGA_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MegaCorp","London","demo@r3.com",null), MEGA_CORP_PUBKEY) +val MEGA_CORP: Party get() = MEGA_CORP_IDENTITY.party +val MINI_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("MiniCorp","London","demo@r3.com",null), MINI_CORP_PUBKEY) +val MINI_CORP: Party get() = MINI_CORP_IDENTITY.party val BOC_KEY: KeyPair by lazy { generateKeyPair() } val BOC_PUBKEY: PublicKey get() = BOC_KEY.public -val BOC: Party get() = Party(getTestX509Name("BankOfCorda"), BOC_PUBKEY) +val BOC_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(getTestX509Name("BankOfCorda"), BOC_PUBKEY) +val BOC: Party get() = BOC_IDENTITY.party val BOC_PARTY_REF = BOC.ref(OpaqueBytes.of(1)).reference val BIG_CORP_KEY: KeyPair by lazy { generateKeyPair() } val BIG_CORP_PUBKEY: PublicKey get() = BIG_CORP_KEY.public -val BIG_CORP: Party get() = Party(X509Utilities.getDevX509Name("BigCorporation"), BIG_CORP_PUBKEY) +val BIG_CORP_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(X509Utilities.getX509Name("BigCorporation","London","demo@r3.com",null), BIG_CORP_PUBKEY) +val BIG_CORP: Party get() = BIG_CORP_IDENTITY.party 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: IdentityService get() = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) +val MOCK_IDENTITIES = listOf(MEGA_CORP_IDENTITY, MINI_CORP_IDENTITY, DUMMY_NOTARY_IDENTITY) +val MOCK_IDENTITY_SERVICE: IdentityService get() = InMemoryIdentityService(MOCK_IDENTITIES, emptyMap(), DUMMY_CA.certificate) val MOCK_VERSION_INFO = VersionInfo(1, "Mock release", "Mock revision", "Mock Vendor") @@ -143,19 +147,6 @@ fun getFreeLocalPorts(hostName: String, numberToAlloc: Int): List { dsl: TransactionDSL.() -> EnforceVerifyOrFail ) = ledger { this.transaction(transactionLabel, transactionBuilder, dsl) } -/** - * The given flow factory will be used to initiate just one instance of a flow of type [P] when a counterparty - * flow requests for it using [clientFlowClass]. - * @return Returns a [ListenableFuture] holding the single [FlowStateMachineImpl] created by the request. - */ -inline fun > AbstractNode.initiateSingleShotFlow( - clientFlowClass: KClass>, - noinline serviceFlowFactory: (Party) -> P): ListenableFuture

{ - val future = smm.changes.filter { it is StateMachineManager.Change.Add && it.logic is P }.map { it.logic as P }.toFuture() - services.registerServiceFlow(clientFlowClass.java, serviceFlowFactory) - return future -} - // TODO Replace this with testConfiguration data class TestNodeConfiguration( override val baseDirectory: Path, @@ -173,7 +164,6 @@ data class TestNodeConfiguration( override val certificateChainCheckPolicies: List = emptyList(), override val verifierType: VerifierType = VerifierType.InMemory, override val messageRedeliveryDelaySeconds: Int = 5) : NodeConfiguration { - override val nearestCity = myLegalName.getRDNs(BCStyle.L).single().typesAndValues.single().value.toString() } fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int): FullNodeConfiguration { @@ -181,7 +171,6 @@ fun testConfiguration(baseDirectory: Path, legalName: X500Name, basePort: Int): basedir = baseDirectory, myLegalName = legalName, networkMapService = null, - nearestCity = "Null Island", emailAddress = "", keyStorePassword = "cordacadevpass", trustStorePassword = "trustpass", @@ -226,4 +215,4 @@ fun getTestX509Name(commonName: String): X500Name { nameBuilder.addRDN(BCStyle.L, "New York") nameBuilder.addRDN(BCStyle.C, "US") return nameBuilder.build() -} \ No newline at end of file +} 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 5bc7af456e..925df7a4ad 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/RPCDriver.kt @@ -13,7 +13,6 @@ import net.corda.core.map import net.corda.core.messaging.RPCOps import net.corda.core.random63BitValue import net.corda.core.utilities.ProcessUtilities -import net.corda.node.driver.* import net.corda.node.services.RPCUserService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.RPCServer @@ -22,6 +21,7 @@ import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.RPCApi import net.corda.nodeapi.User +import net.corda.testing.driver.* import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.TransportConfiguration import org.apache.activemq.artemis.api.core.client.ActiveMQClient @@ -224,6 +224,7 @@ fun rpcDriver( systemProperties: Map = emptyMap(), useTestClock: Boolean = false, networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), + startNodesInProcess: Boolean = false, dsl: RPCDriverExposedDSLInterface.() -> A ) = genericDriver( driverDsl = RPCDriverDSL( @@ -234,7 +235,8 @@ fun rpcDriver( driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, networkMapStartStrategy = networkMapStartStrategy, - isDebug = isDebug + isDebug = isDebug, + startNodesInProcess = startNodesInProcess ) ), coerce = { it }, @@ -419,7 +421,7 @@ data class RPCDriverDSL( server.start() driverDSL.shutdownManager.registerShutdown { server.stop() - addressMustNotBeBound(driverDSL.executorService, hostAndPort).get() + addressMustNotBeBound(driverDSL.executorService, hostAndPort) } RpcBrokerHandle( hostAndPort = hostAndPort, diff --git a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt index 8566851baa..7a0e41094b 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TestDSL.kt @@ -145,8 +145,8 @@ data class TestTransactionDSLInterpreter private constructor( return EnforceVerifyOrFail.Token } - override fun timestamp(data: Timestamp) { - transactionBuilder.setTime(data) + override fun timeWindow(data: TimeWindow) { + transactionBuilder.addTimeWindow(data) } override fun tweak( 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 27d69dcc2e..b806e699de 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/TransactionDSLInterpreter.kt @@ -1,8 +1,8 @@ package net.corda.testing import net.corda.core.contracts.* -import net.corda.core.identity.Party import net.corda.core.crypto.SecureHash +import net.corda.core.identity.Party import net.corda.core.seconds import net.corda.core.transactions.TransactionBuilder import net.corda.core.utilities.DUMMY_NOTARY @@ -51,10 +51,10 @@ interface TransactionDSLInterpreter : Verifies, OutputStateLookup { fun _command(signers: List, commandData: CommandData) /** - * Adds a timestamp to the transaction. - * @param data The [TimestampCommand]. + * Adds a time-window to the transaction. + * @param data the [TimeWindow] (validation window). */ - fun timestamp(data: Timestamp) + fun timeWindow(data: TimeWindow) /** * Creates a local scoped copy of the transaction. @@ -115,11 +115,11 @@ class TransactionDSL(val interpreter: T) : Tr fun command(signer: PublicKey, commandData: CommandData) = _command(listOf(signer), commandData) /** - * Adds a timestamp command to the transaction. - * @param time The [Instant] of the [TimestampCommand]. - * @param tolerance The tolerance of the [TimestampCommand]. + * Adds a [TimeWindow] command to the transaction. + * @param time The [Instant] of the [TimeWindow]. + * @param tolerance The tolerance of the [TimeWindow]. */ @JvmOverloads - fun timestamp(time: Instant, tolerance: Duration = 30.seconds) = - timestamp(Timestamp(time, tolerance)) + fun timeWindow(time: Instant, tolerance: Duration = 30.seconds) = + timeWindow(TimeWindow.withTolerance(time, tolerance)) } diff --git a/node/src/main/kotlin/net/corda/node/driver/Driver.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt similarity index 70% rename from node/src/main/kotlin/net/corda/node/driver/Driver.kt rename to test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt index 65ae64bd26..8dca081231 100644 --- a/node/src/main/kotlin/net/corda/node/driver/Driver.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/Driver.kt @@ -1,12 +1,14 @@ @file:JvmName("Driver") -package net.corda.node.driver +package net.corda.testing.driver import com.google.common.net.HostAndPort import com.google.common.util.concurrent.* import com.typesafe.config.Config import com.typesafe.config.ConfigRenderOptions import net.corda.client.rpc.CordaRPCClient +import net.corda.cordform.CordformContext +import net.corda.cordform.CordformNode import net.corda.core.* import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName @@ -17,23 +19,21 @@ import net.corda.core.node.NodeInfo 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.internal.Node +import net.corda.node.internal.NodeStartup +import net.corda.node.serialization.NodeClock 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 +import net.corda.node.utilities.TestClock 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 net.corda.core.internal.ShutdownHook -import net.corda.core.internal.addShutdownHook +import net.corda.nodeapi.internal.ShutdownHook +import net.corda.nodeapi.internal.addShutdownHook +import net.corda.testing.MOCK_VERSION_INFO import okhttp3.OkHttpClient import okhttp3.Request import org.bouncycastle.asn1.x500.X500Name @@ -42,6 +42,7 @@ import java.io.File import java.net.* import java.nio.file.Path import java.nio.file.Paths +import java.time.Clock import java.time.Duration import java.time.Instant import java.time.ZoneOffset.UTC @@ -51,6 +52,7 @@ import java.util.concurrent.* import java.util.concurrent.TimeUnit.MILLISECONDS import java.util.concurrent.TimeUnit.SECONDS import java.util.concurrent.atomic.AtomicInteger +import kotlin.concurrent.thread /** @@ -58,6 +60,8 @@ import java.util.concurrent.atomic.AtomicInteger * * The process the driver is run in behaves as an Artemis client and starts up other processes. Namely it first * bootstraps a network map service to allow the specified nodes to connect to, then starts up the actual nodes. + * + * TODO this file is getting way too big, it should be split into several files. */ private val log: Logger = loggerFor() @@ -70,19 +74,25 @@ interface DriverDSLExposedInterface : CordformContext { * Starts a [net.corda.node.internal.Node] in a separate process. * * @param providedName Optional name of the node, which will be its legal name in [Party]. Defaults to something - * random. Note that this must be unique as the driver uses it as a primary key! + * random. Note that this must be unique as the driver uses it as a primary key! * @param advertisedServices The set of services to be advertised by the node. Defaults to empty set. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. + * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running + * in. If null the Driver-level value will be used. * @return The [NodeInfo] of the started up node retrieved from the network map service. */ fun startNode(providedName: X500Name? = null, advertisedServices: Set = emptySet(), rpcUsers: List = emptyList(), verifierType: VerifierType = VerifierType.InMemory, - customOverrides: Map = emptyMap()): ListenableFuture + customOverrides: Map = emptyMap(), + startInSameProcess: Boolean? = null): ListenableFuture - fun startNodes(nodes: List): List> + fun startNodes( + nodes: List, + startInSameProcess: Boolean? = null + ): List> /** * Starts a distributed notary cluster. @@ -92,6 +102,8 @@ interface DriverDSLExposedInterface : CordformContext { * @param type The advertised notary service type. Currently the only supported type is [RaftValidatingNotaryService.type]. * @param verifierType The type of transaction verifier to use. See: [VerifierType] * @param rpcUsers List of users who are authorised to use the RPC system. Defaults to empty list. + * @param startInSameProcess Determines if the node should be started inside the same process the Driver is running + * in. If null the Driver-level value will be used. * @return The [Party] identity of the distributed notary service, and the [NodeInfo]s of the notaries in the cluster. */ fun startNotaryCluster( @@ -99,7 +111,8 @@ interface DriverDSLExposedInterface : CordformContext { clusterSize: Int = 3, type: ServiceType = RaftValidatingNotaryService.type, verifierType: VerifierType = VerifierType.InMemory, - rpcUsers: List = emptyList()): Future>> + rpcUsers: List = emptyList(), + startInSameProcess: Boolean? = null): ListenableFuture>> /** * Starts a web server for a node @@ -111,8 +124,10 @@ interface DriverDSLExposedInterface : CordformContext { /** * Starts a network map service node. Note that only a single one should ever be running, so you will probably want * to set networkMapStartStrategy to Dedicated(false) in your [driver] call. + * @param startInProcess Determines if the node should be started inside this process. If null the Driver-level + * value will be used. */ - fun startDedicatedNetworkMapService(): ListenableFuture + fun startDedicatedNetworkMapService(startInProcess: Boolean? = null): ListenableFuture fun waitForAllNodesToFinish() @@ -142,13 +157,30 @@ interface DriverDSLInternalInterface : DriverDSLExposedInterface { fun shutdown() } -data class NodeHandle( - val nodeInfo: NodeInfo, - val rpc: CordaRPCOps, - val configuration: FullNodeConfiguration, - val webAddress: HostAndPort, - val process: Process -) { +sealed class NodeHandle { + abstract val nodeInfo: NodeInfo + abstract val rpc: CordaRPCOps + abstract val configuration: FullNodeConfiguration + abstract val webAddress: HostAndPort + + data class OutOfProcess( + override val nodeInfo: NodeInfo, + override val rpc: CordaRPCOps, + override val configuration: FullNodeConfiguration, + override val webAddress: HostAndPort, + val debugPort: Int?, + val process: Process + ) : NodeHandle() + + data class InProcess( + override val nodeInfo: NodeInfo, + override val rpc: CordaRPCOps, + override val configuration: FullNodeConfiguration, + override val webAddress: HostAndPort, + val node: Node, + val nodeThread: Thread + ) : NodeHandle() + fun rpcClientToNode(): CordaRPCClient = CordaRPCClient(configuration.rpcAddress!!) } @@ -190,6 +222,7 @@ sealed class PortAllocation { * * The driver implicitly bootstraps a [NetworkMapService]. * + * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. * @param driverDirectory The base directory node directories go into, defaults to "build//". The node * directories themselves are "//", where legalName defaults to "-" * and may be specified in [DriverDSL.startNode]. @@ -197,7 +230,9 @@ sealed class PortAllocation { * @param debugPortAllocation The port allocation strategy to use for jvm debugging. Defaults to incremental. * @param systemProperties A Map of extra system properties which will be given to each new node. Defaults to empty. * @param useTestClock If true the test clock will be used in Node. - * @param isDebug Indicates whether the spawned nodes should start in jdwt debug mode and have debug level logging. + * @param networkMapStartStrategy Determines whether a network map node is started automatically. + * @param startNodesInProcess Provides the default behaviour of whether new nodes should start inside this process or + * not. Note that this may be overridden in [DriverDSLExposedInterface.startNode]. * @param dsl The dsl itself. * @return The value returned in the [dsl] closure. */ @@ -210,6 +245,7 @@ fun driver( systemProperties: Map = emptyMap(), useTestClock: Boolean = false, networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = true), + startNodesInProcess: Boolean = false, dsl: DriverDSLExposedInterface.() -> A ) = genericDriver( driverDsl = DriverDSL( @@ -219,6 +255,7 @@ fun driver( driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, networkMapStartStrategy = networkMapStartStrategy, + startNodesInProcess = startNodesInProcess, isDebug = isDebug ), coerce = { it }, @@ -263,9 +300,13 @@ class ListenProcessDeathException(message: String) : Exception(message) /** * @throws ListenProcessDeathException if [listenProcess] dies before the check succeeds, i.e. the check can't succeed as intended. */ -fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process): ListenableFuture { +fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null) { + addressMustBeBoundFuture(executorService, hostAndPort, listenProcess).getOrThrow() +} + +fun addressMustBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort, listenProcess: Process? = null): ListenableFuture { return poll(executorService, "address $hostAndPort to bind") { - if (!listenProcess.isAlive) { + if (listenProcess != null && !listenProcess.isAlive) { throw ListenProcessDeathException("The process that was expected to listen on $hostAndPort has died with status: ${listenProcess.exitValue()}") } try { @@ -277,7 +318,11 @@ fun addressMustBeBound(executorService: ScheduledExecutorService, hostAndPort: H } } -fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture { +fun addressMustNotBeBound(executorService: ScheduledExecutorService, hostAndPort: HostAndPort) { + addressMustNotBeBoundFuture(executorService, hostAndPort).getOrThrow() +} + +fun addressMustNotBeBoundFuture(executorService: ScheduledExecutorService, hostAndPort: HostAndPort): ListenableFuture { return poll(executorService, "address $hostAndPort to unbind") { try { Socket(hostAndPort.host, hostAndPort.port).close() @@ -333,6 +378,19 @@ class ShutdownManager(private val executorService: ExecutorService) { private val state = ThreadBox(State()) + companion object { + inline fun run(providedExecutorService: ExecutorService? = null, block: ShutdownManager.() -> A): A { + val executorService = providedExecutorService ?: Executors.newScheduledThreadPool(1) + val shutdownManager = ShutdownManager(executorService) + try { + return block(shutdownManager) + } finally { + shutdownManager.shutdown() + providedExecutorService ?: executorService.shutdown() + } + } + } + fun shutdown() { val shutdownFutures = state.locked { if (isShutdown) { @@ -417,13 +475,15 @@ class DriverDSL( val driverDirectory: Path, val useTestClock: Boolean, val isDebug: Boolean, - val networkMapStartStrategy: NetworkMapStartStrategy + val networkMapStartStrategy: NetworkMapStartStrategy, + val startNodesInProcess: Boolean ) : DriverDSLInternalInterface { private val dedicatedNetworkMapAddress = portAllocation.nextHostAndPort() - var _executorService: ListeningScheduledExecutorService? = null + private var _executorService: ListeningScheduledExecutorService? = null val executorService get() = _executorService!! - var _shutdownManager: ShutdownManager? = null + private var _shutdownManager: ShutdownManager? = null override val shutdownManager get() = _shutdownManager!! + private val callerPackage = getCallerPackage() class State { val processes = ArrayList>() @@ -496,57 +556,50 @@ class DriverDSL( advertisedServices: Set, rpcUsers: List, verifierType: VerifierType, - customOverrides: Map): ListenableFuture { + customOverrides: Map, + startInSameProcess: Boolean? + ): ListenableFuture { val p2pAddress = portAllocation.nextHostAndPort() val rpcAddress = portAllocation.nextHostAndPort() val webAddress = portAllocation.nextHostAndPort() // TODO: Derive name from the full picked name, don't just wrap the common name - 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 networkMapServiceConfigLookup(emptyList())(name), - "useTestClock" to useTestClock, - "rpcUsers" to rpcUsers.map { it.toMap() }, - "verifierType" to verifierType.name - ) + customOverrides) - } - - private fun startNode(p2pAddress: HostAndPort, webAddress: HostAndPort, nodeName: X500Name, configOverrides: Config) = run { - val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + val name = providedName ?: X509Utilities.getX509Name("${oneOf(names).commonName}-${p2pAddress.port}","London","demo@r3.com",null) val config = ConfigHelper.loadConfig( - baseDirectory = baseDirectory(nodeName), + baseDirectory = baseDirectory(name), allowMissingConfig = true, - configOverrides = configOverrides) - val configuration = config.parseAs() - val processFuture = startNode(executorService, configuration, config, quasarJarPath, debugPort, systemProperties) - registerProcess(processFuture) - 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 { - NodeHandle(rpc.nodeIdentity(), rpc, configuration, webAddress, process) - } - } - } + configOverrides = 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 networkMapServiceConfigLookup(emptyList())(name), + "useTestClock" to useTestClock, + "rpcUsers" to rpcUsers.map { it.toMap() }, + "verifierType" to verifierType.name + ) + customOverrides + ) + return startNodeInternal(config, webAddress, startInSameProcess) } - override fun startNodes(nodes: List): List> { + override fun startNodes(nodes: List, startInSameProcess: Boolean?): List> { val networkMapServiceConfigLookup = networkMapServiceConfigLookup(nodes) - return nodes.map { - val p2pAddress = HostAndPort.fromString(it.config.getString("p2pAddress")); portAllocation.nextHostAndPort() + return nodes.map { node -> 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 - )) + val name = X500Name(node.name) + + val config = ConfigHelper.loadConfig( + baseDirectory = baseDirectory(name), + allowMissingConfig = true, + configOverrides = node.config + mapOf( + "extraAdvertisedServiceIds" to node.advertisedServices, + "networkMapService" to networkMapServiceConfigLookup(name), + "rpcUsers" to node.rpcUsers, + "notaryClusterAddresses" to node.notaryClusterAddresses + ) + ) + startNodeInternal(config, webAddress, startInSameProcess) } } @@ -555,7 +608,8 @@ class DriverDSL( clusterSize: Int, type: ServiceType, verifierType: VerifierType, - rpcUsers: List + rpcUsers: List, + startInSameProcess: Boolean? ): ListenableFuture>> { val nodeNames = (0 until clusterSize).map { DUMMY_NOTARY.name.appendToCommonName(" $it") } val paths = nodeNames.map { baseDirectory(it) } @@ -564,7 +618,14 @@ class DriverDSL( val notaryClusterAddress = portAllocation.nextHostAndPort() // Start the first node that will bootstrap the cluster - val firstNotaryFuture = startNode(nodeNames.first(), advertisedServices, rpcUsers, verifierType, mapOf("notaryNodeAddress" to notaryClusterAddress.toString())) + val firstNotaryFuture = startNode( + providedName = nodeNames.first(), + advertisedServices = advertisedServices, + rpcUsers = rpcUsers, + verifierType = verifierType, + customOverrides = mapOf("notaryNodeAddress" to notaryClusterAddress.toString()), + startInSameProcess = startInSameProcess + ) // All other nodes will join the cluster val restNotaryFutures = nodeNames.drop(1).map { val nodeAddress = portAllocation.nextHostAndPort() @@ -609,16 +670,17 @@ class DriverDSL( Executors.newScheduledThreadPool(2, ThreadFactoryBuilder().setNameFormat("driver-pool-thread-%d").build()) ) _shutdownManager = ShutdownManager(executorService) + // We set this property so that in-process nodes find cordapps. Out-of-process nodes need this passed in when started. + System.setProperty("net.corda.node.cordapp.scan.package", callerPackage) if (networkMapStartStrategy.startDedicated) { - startDedicatedNetworkMapService() + startDedicatedNetworkMapService().andForget(log) // Allow it to start concurrently with other nodes. } } - override fun baseDirectory(nodeName: X500Name) = driverDirectory / nodeName.commonName.replace(WHITESPACE, "") + override fun baseDirectory(nodeName: X500Name): Path = driverDirectory / nodeName.commonName.replace(WHITESPACE, "") - override fun startDedicatedNetworkMapService(): ListenableFuture { - val debugPort = if (isDebug) debugPortAllocation.nextPort() else null - val apiAddress = portAllocation.nextHostAndPort().toString() + override fun startDedicatedNetworkMapService(startInProcess: Boolean?): ListenableFuture { + val webAddress = portAllocation.nextHostAndPort() val networkMapLegalName = networkMapStartStrategy.legalName val config = ConfigHelper.loadConfig( baseDirectory = baseDirectory(networkMapLegalName), @@ -627,16 +689,44 @@ class DriverDSL( "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, + "webAddress" to webAddress.toString(), "p2pAddress" to dedicatedNetworkMapAddress.toString(), "useTestClock" to useTestClock ) ) + return startNodeInternal(config, webAddress, startInProcess) + } - 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) } + private fun startNodeInternal(config: Config, webAddress: HostAndPort, startInProcess: Boolean?): ListenableFuture { + val nodeConfiguration = config.parseAs() + if (startInProcess ?: startNodesInProcess) { + val nodeAndThreadFuture = startInProcessNode(executorService, nodeConfiguration, config) + shutdownManager.registerShutdown( + nodeAndThreadFuture.map { (node, thread) -> { + node.stop() + thread.interrupt() + } } + ) + return nodeAndThreadFuture.flatMap { (node, thread) -> + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> + rpc.waitUntilRegisteredWithNetworkMap().map { + NodeHandle.InProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, node, thread) + } + } + } + } else { + val debugPort = if (isDebug) debugPortAllocation.nextPort() else null + val processFuture = startOutOfProcessNode(executorService, nodeConfiguration, config, quasarJarPath, debugPort, systemProperties, callerPackage) + registerProcess(processFuture) + return processFuture.flatMap { process -> + // We continue to use SSL enabled port for RPC when its for node user. + establishRpc(nodeConfiguration.p2pAddress, nodeConfiguration).flatMap { rpc -> + rpc.waitUntilRegisteredWithNetworkMap().map { + NodeHandle.OutOfProcess(rpc.nodeIdentity(), rpc, nodeConfiguration, webAddress, debugPort, process) + } + } + } + } } override fun pollUntilNonNull(pollName: String, pollInterval: Duration, warnCount: Int, check: () -> A?): ListenableFuture { @@ -654,21 +744,44 @@ class DriverDSL( private fun oneOf(array: Array) = array[Random().nextInt(array.size)] - private fun startNode( + private fun startInProcessNode( + executorService: ListeningScheduledExecutorService, + nodeConf: FullNodeConfiguration, + config: Config + ): ListenableFuture> { + return executorService.submit> { + log.info("Starting in-process Node ${nodeConf.myLegalName.commonName}") + // Write node.conf + writeConfig(nodeConf.baseDirectory, "node.conf", config) + val clock: Clock = if (nodeConf.useTestClock) TestClock() else NodeClock() + // TODO pass the version in? + val node = Node(nodeConf, nodeConf.calculateServices(), MOCK_VERSION_INFO, clock) + node.start() + val nodeThread = thread(name = nodeConf.myLegalName.commonName) { + node.run() + } + node to nodeThread + }.flatMap { nodeAndThread -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress).map { nodeAndThread } } + } + + private fun startOutOfProcessNode( executorService: ListeningScheduledExecutorService, nodeConf: FullNodeConfiguration, config: Config, quasarJarPath: String, debugPort: Int?, - overriddenSystemProperties: Map + overriddenSystemProperties: Map, + callerPackage: String ): ListenableFuture { - return executorService.submit { + val processFuture = executorService.submit { + log.info("Starting out-of-process Node ${nodeConf.myLegalName.commonName}") // Write node.conf writeConfig(nodeConf.baseDirectory, "node.conf", config) val systemProperties = overriddenSystemProperties + mapOf( "name" to nodeConf.myLegalName, "visualvm.display.name" to "corda-${nodeConf.myLegalName}", + "net.corda.node.cordapp.scan.package" to callerPackage, "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. @@ -688,10 +801,13 @@ class DriverDSL( ), jdwpPort = debugPort, extraJvmArguments = extraJvmArguments, - errorLogPath = nodeConf.baseDirectory / LOGS_DIRECTORY_NAME / "error.log", + errorLogPath = nodeConf.baseDirectory / NodeStartup.LOGS_DIRECTORY_NAME / "error.log", workingDirectory = nodeConf.baseDirectory ) - }.flatMap { process -> addressMustBeBound(executorService, nodeConf.p2pAddress, process).map { process } } + } + return processFuture.flatMap { + process -> addressMustBeBoundFuture(executorService, nodeConf.p2pAddress, process).map { process } + } } private fun startWebserver( @@ -711,7 +827,14 @@ class DriverDSL( ), errorLogPath = Paths.get("error.$className.log") ) - }.flatMap { process -> addressMustBeBound(executorService, handle.webAddress, process).map { process } } + }.flatMap { process -> addressMustBeBoundFuture(executorService, handle.webAddress, process).map { process } } + } + + private fun getCallerPackage(): String { + return Exception() + .stackTrace + .first { it.fileName != "Driver.kt" } + .let { Class.forName(it.className).`package`.name } } } } diff --git a/node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt similarity index 95% rename from node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt rename to test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt index 218dfe5716..93a42686d4 100644 --- a/node/src/main/kotlin/net/corda/node/driver/NetworkMapStartStrategy.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/driver/NetworkMapStartStrategy.kt @@ -1,4 +1,4 @@ -package net.corda.node.driver +package net.corda.testing.driver import com.google.common.net.HostAndPort import net.corda.core.utilities.DUMMY_MAP diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt index c6f7da5705..42b3221264 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpApi.kt @@ -1,9 +1,10 @@ package net.corda.testing.http +import com.fasterxml.jackson.databind.ObjectMapper import com.google.common.net.HostAndPort import java.net.URL -class HttpApi(val root: URL) { +class HttpApi(val root: URL, val mapper: ObjectMapper = defaultMapper) { /** * Send a PUT with a payload to the path on the API specified. * @@ -21,12 +22,15 @@ class HttpApi(val root: URL) { /** * Send a GET request to the path on the API specified. */ - inline fun getJson(path: String, params: Map = mapOf()) = HttpUtils.getJson(URL(root, path), params) + inline fun getJson(path: String, params: Map = mapOf()) = HttpUtils.getJson(URL(root, path), params, mapper) private fun toJson(any: Any) = any as? String ?: HttpUtils.defaultMapper.writeValueAsString(any) companion object { - fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http"): HttpApi - = HttpApi(URL("$protocol://$hostAndPort/$base/")) + fun fromHostAndPort(hostAndPort: HostAndPort, base: String, protocol: String = "http", mapper: ObjectMapper = defaultMapper): HttpApi + = HttpApi(URL("$protocol://$hostAndPort/$base/"), mapper) + private val defaultMapper: ObjectMapper by lazy { + net.corda.jackson.JacksonSupport.createNonRpcMapper() + } } } diff --git a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt index 9b2e454f13..94eef77537 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/http/HttpUtils.kt @@ -35,10 +35,10 @@ object HttpUtils { return makeRequest(Request.Builder().url(url).header("Content-Type", "application/json").post(body).build()) } - inline fun getJson(url: URL, params: Map = mapOf()): T { + inline fun getJson(url: URL, params: Map = mapOf(), mapper: ObjectMapper = defaultMapper): T { val paramString = if (params.isEmpty()) "" else "?" + params.map { "${it.key}=${it.value}" }.joinToString("&") val parameterisedUrl = URL(url.toExternalForm() + paramString) - return defaultMapper.readValue(parameterisedUrl, T::class.java) + return mapper.readValue(parameterisedUrl, T::class.java) } private fun makeRequest(request: Request): Boolean { diff --git a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt index 247db93a88..dd521c8671 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/messaging/SimpleMQClient.kt @@ -15,7 +15,7 @@ import org.bouncycastle.asn1.x500.X500Name class SimpleMQClient(val target: HostAndPort, override val config: SSLConfiguration? = configureTestSSL(DEFAULT_MQ_LEGAL_NAME)) : ArtemisMessagingComponent() { companion object { - val DEFAULT_MQ_LEGAL_NAME = X500Name("CN=SimpleMQClient,O=R3,OU=corda,L=London,C=UK") + val DEFAULT_MQ_LEGAL_NAME = X500Name("CN=SimpleMQClient,O=R3,OU=corda,L=London,C=GB") } lateinit var sessionFactory: ClientSessionFactory lateinit var session: ClientSession @@ -42,4 +42,4 @@ class SimpleMQClient(val target: HostAndPort, // sessionFactory might not have initialised. } } -} \ No newline at end of file +} diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt b/test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt index c2fe6d2048..6c9219dfb4 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/DriverBasedTest.kt @@ -2,7 +2,7 @@ package net.corda.testing.node import com.google.common.util.concurrent.SettableFuture import net.corda.core.getOrThrow -import net.corda.node.driver.DriverDSLExposedInterface +import net.corda.testing.driver.DriverDSLExposedInterface import org.junit.After import org.junit.Before import java.util.concurrent.CountDownLatch diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt index c2d1cf310b..ffe15ee54a 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/InMemoryMessagingNetwork.kt @@ -130,7 +130,7 @@ class InMemoryMessagingNetwork( description: X500Name? = null, database: Database) : MessagingServiceBuilder { - return Builder(manuallyPumped, PeerHandle(id, description ?: X509Utilities.getDevX509Name("In memory node $id")), advertisedServices.map(::ServiceHandle), executor, database = database) + return Builder(manuallyPumped, PeerHandle(id, description ?: X509Utilities.getX509Name("In memory node $id","London","demo@r3.com",null)), advertisedServices.map(::ServiceHandle), executor, database = database) } interface LatencyCalculator { diff --git a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt index 76790b851b..47bd3bf6e2 100644 --- a/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt +++ b/test-utils/src/main/kotlin/net/corda/testing/node/MockNetworkMapCache.kt @@ -1,24 +1,26 @@ package net.corda.testing.node import co.paralleluniverse.common.util.VisibleForTesting -import net.corda.core.crypto.DummyPublicKey +import net.corda.core.crypto.entropyToKeyPair import net.corda.core.identity.Party import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.services.NetworkMapCache +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.services.network.InMemoryNetworkMapCache import net.corda.testing.MOCK_VERSION_INFO import net.corda.testing.getTestX509Name import rx.Observable import rx.subjects.PublishSubject +import java.math.BigInteger /** * Network map cache with no backing map service. */ class MockNetworkMapCache : InMemoryNetworkMapCache() { private companion object { - val BANK_C = Party(getTestX509Name("Bank C"), DummyPublicKey("Bank C")) - val BANK_D = Party(getTestX509Name("Bank D"), DummyPublicKey("Bank D")) + val BANK_C = getTestPartyAndCertificate(getTestX509Name("Bank C"), entropyToKeyPair(BigInteger.valueOf(1000)).public) + val BANK_D = getTestPartyAndCertificate(getTestX509Name("Bank D"), entropyToKeyPair(BigInteger.valueOf(2000)).public) } override val changed: Observable = PublishSubject.create() 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 4905acec1d..55ddd92b92 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 @@ -1,14 +1,14 @@ package net.corda.testing.node -import com.google.common.annotations.VisibleForTesting import com.google.common.jimfs.Configuration.unix import com.google.common.jimfs.Jimfs import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture import net.corda.core.* import net.corda.core.crypto.entropyToKeyPair -import net.corda.core.flows.FlowLogic -import net.corda.core.identity.Party +import net.corda.flows.TxKeyFlow +import net.corda.core.identity.PartyAndCertificate +import net.corda.core.messaging.MessageRecipients import net.corda.core.messaging.RPCOps import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.CordaPluginRegistry @@ -16,16 +16,15 @@ import net.corda.core.node.PhysicalLocation import net.corda.core.node.ServiceEntry import net.corda.core.node.services.* import net.corda.core.utilities.DUMMY_NOTARY_KEY +import net.corda.core.utilities.getTestPartyAndCertificate 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 import net.corda.node.services.network.NetworkMapService -import net.corda.node.services.statemachine.flowVersion import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.transactions.InMemoryUniquenessProvider import net.corda.node.services.transactions.SimpleNotaryService @@ -42,10 +41,10 @@ import org.slf4j.Logger import java.math.BigInteger import java.nio.file.FileSystem import java.security.KeyPair +import java.security.cert.X509Certificate import java.util.* import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger -import kotlin.reflect.KClass /** * A mock node brings up a suite of in-memory services in a fast manner suitable for unit testing. @@ -73,7 +72,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // A unique identifier for this network to segregate databases with the same nodeID but different networks. private val networkId = random63BitValue() - val identities = ArrayList() + val identities = ArrayList() private val _nodes = ArrayList() /** A read only view of the current set of executing nodes. */ @@ -167,12 +166,15 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, .getOrThrow() } - override fun makeIdentityService() = InMemoryIdentityService(mockNet.identities) + // TODO: Specify a CA to validate registration against + override fun makeIdentityService(): IdentityService { + return InMemoryIdentityService((mockNet.identities + info.legalIdentityAndCert).toSet(), trustRoot = null as X509Certificate?) + } override fun makeVaultService(dataSourceProperties: Properties): VaultService = NodeVaultService(services, dataSourceProperties) - override fun makeKeyManagementService(): KeyManagementService { - return E2ETestKeyManagementService(partyKeys + (overrideServices?.values ?: emptySet())) + override fun makeKeyManagementService(identityService: IdentityService): KeyManagementService { + return E2ETestKeyManagementService(identityService, partyKeys + (overrideServices?.values ?: emptySet())) } override fun startMessagingService(rpcOps: RPCOps) { @@ -192,7 +194,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val override = overrideServices[it.info] if (override != null) { // TODO: Store the key - ServiceEntry(it.info, Party(it.identity.name, override.public)) + ServiceEntry(it.info, getTestPartyAndCertificate(it.identity.name, override.public)) } else { it } @@ -218,7 +220,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, override fun start(): MockNode { super.start() - mockNet.identities.add(info.legalIdentity) + mockNet.identities.add(info.legalIdentityAndCert) return this } @@ -232,15 +234,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, // It is used from the network visualiser tool. @Suppress("unused") val place: PhysicalLocation get() = findMyLocation()!! - @VisibleForTesting - fun registerServiceFlow(clientFlowClass: KClass>, - flowVersion: Int = clientFlowClass.java.flowVersion, - serviceFlowFactory: (Party) -> FlowLogic<*>) { - serviceFlowFactories[clientFlowClass.java] = ServiceFlowInfo.CorDapp(flowVersion, serviceFlowFactory) - } - fun pumpReceive(block: Boolean = false): InMemoryMessagingNetwork.MessageTransfer? { - return (net as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) + return (network as InMemoryMessagingNetwork.InMemoryMessaging).pumpReceive(block) } fun disableDBCloseOnStop() { @@ -248,7 +243,7 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } fun manuallyCloseDB() { - dbCloser?.run() + dbCloser?.invoke() dbCloser = null } @@ -262,8 +257,6 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, * Returns a node, optionally created by the passed factory method. * @param overrideServices a set of service entries to use in place of the node's default service entries, * for example where a node's service is part of a cluster. - * @param entropyRoot the initial entropy value to use when generating keys. Defaults to an (insecure) random value, - * but can be overriden to cause nodes to have stable or colliding identity/service keys. */ fun createNode(networkMapAddress: SingleMessageRecipient? = null, forcedID: Int = -1, nodeFactory: Factory = defaultFactory, start: Boolean = true, legalName: X500Name? = null, overrideServices: Map? = null, @@ -296,6 +289,8 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, val node = nodeFactory.create(config, this, networkMapAddress, advertisedServices.toSet(), id, overrideServices, entropyRoot) if (start) { node.setup().start() + // Register flows that are normally found via plugins + node.registerInitiatedFlow(TxKeyFlow.Provider::class.java) if (threadPerNode && networkMapAddress != null) node.networkMapRegistrationFuture.getOrThrow() // Block and wait for the node to register in the net map. } @@ -370,6 +365,9 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, repeat(numPartyNodes) { nodes += createPartyNode(mapNode.info.address) } + nodes.forEach { itNode -> + nodes.map { it.info.legalIdentityAndCert }.forEach(itNode.services.identityService::registerIdentity) + } return BasketOfNodes(nodes, notaryNode, mapNode) } @@ -388,7 +386,16 @@ class MockNetwork(private val networkSendManuallyPumped: Boolean = false, } @Suppress("unused") // This is used from the network visualiser tool. - fun addressToNode(address: SingleMessageRecipient): MockNode = nodes.single { it.net.myAddress == address } + fun addressToNode(msgRecipient: MessageRecipients): MockNode { + return when (msgRecipient) { + is SingleMessageRecipient -> nodes.single { it.network.myAddress == msgRecipient } + is InMemoryMessagingNetwork.ServiceHandle -> { + nodes.filter { it.advertisedServices.any { it == msgRecipient.service.info } }.firstOrNull() + ?: throw IllegalArgumentException("Couldn't find node advertising service with info: ${msgRecipient.service.info} ") + } + else -> throw IllegalArgumentException("Method not implemented for different type of message recipients") + } + } fun startNodes() { require(nodes.isNotEmpty()) 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 7eb9e8c5dc..e390bb3fbe 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 @@ -1,44 +1,45 @@ package net.corda.testing.node 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.identity.PartyAndCertificate import net.corda.core.messaging.SingleMessageRecipient import net.corda.core.node.NodeInfo import net.corda.core.node.ServiceHub import net.corda.core.node.services.* +import net.corda.core.serialization.SerializeAsToken import net.corda.core.serialization.SingletonSerializeAsToken import net.corda.core.transactions.SignedTransaction -import net.corda.core.utilities.DUMMY_NOTARY +import net.corda.core.utilities.DUMMY_CA +import net.corda.core.utilities.getTestPartyAndCertificate import net.corda.node.services.identity.InMemoryIdentityService +import net.corda.node.services.keys.freshCertificate +import net.corda.node.services.keys.getSigner import net.corda.node.services.persistence.InMemoryStateMachineRecordedTransactionMappingStorage import net.corda.node.services.schema.HibernateObserver import net.corda.node.services.schema.NodeSchemaService import net.corda.node.services.transactions.InMemoryTransactionVerifierService import net.corda.node.services.vault.NodeVaultService import net.corda.testing.MEGA_CORP -import net.corda.testing.MINI_CORP +import net.corda.testing.MOCK_IDENTITIES import net.corda.testing.MOCK_VERSION_INFO -import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.cert.X509CertificateHolder +import org.bouncycastle.operator.ContentSigner import rx.Observable import rx.subjects.PublishSubject import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream +import java.nio.file.Path 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 @@ -64,13 +65,13 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { } override val storageService: TxWritableStorageService = MockStorageService() - override val identityService: IdentityService = InMemoryIdentityService(listOf(MEGA_CORP, MINI_CORP, DUMMY_NOTARY)) - override val keyManagementService: KeyManagementService = MockKeyManagementService(*keys) + override final val identityService: IdentityService = InMemoryIdentityService(MOCK_IDENTITIES, trustRoot = DUMMY_CA.certificate) + override val keyManagementService: KeyManagementService = MockKeyManagementService(identityService, *keys) override val vaultService: VaultService get() = throw UnsupportedOperationException() override val networkMapCache: NetworkMapCache get() = throw UnsupportedOperationException() override val clock: Clock get() = Clock.systemUTC() - override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, Party(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) + override val myInfo: NodeInfo get() = NodeInfo(object : SingleMessageRecipient {}, getTestPartyAndCertificate(MEGA_CORP.name, key.public), MOCK_VERSION_INFO.platformVersion) override val transactionVerifierService: TransactionVerifierService get() = InMemoryTransactionVerifierService(2) fun makeVaultService(dataSourceProps: Properties): VaultService { @@ -79,9 +80,12 @@ open class MockServices(vararg val keys: KeyPair) : ServiceHub { HibernateObserver(vaultService.rawUpdates, NodeSchemaService()) return vaultService } + + override fun cordaService(type: Class): T = throw IllegalArgumentException("${type.name} not found") } -class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { +class MockKeyManagementService(val identityService: IdentityService, + vararg initialKeys: KeyPair) : SingletonSerializeAsToken(), KeyManagementService { private val keyStore: MutableMap = initialKeys.associateByTo(HashMap(), { it.public }, { it.private }) override val keys: Set get() = keyStore.keys @@ -94,6 +98,12 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerialize return k.public } + override fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair { + return freshCertificate(identityService, freshKey(), identity, getSigner(identity.owningKey), revocationEnabled) + } + + private fun getSigner(publicKey: PublicKey): ContentSigner = getSigner(getSigningKeyPair(publicKey)) + private fun getSigningKeyPair(publicKey: PublicKey): KeyPair { val pk = publicKey.keys.first { keyStore.containsKey(it) } return KeyPair(pk, keyStore[pk]!!) @@ -109,7 +119,7 @@ class MockKeyManagementService(vararg initialKeys: KeyPair) : SingletonSerialize class MockAttachmentStorage : AttachmentStorage { val files = HashMap() override var automaticallyExtractAttachments = false - override var storePath = Paths.get("") + override var storePath: Path = Paths.get("") override fun openAttachment(id: SecureHash): Attachment? { val f = files[id] ?: return null 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 7b44b39910..cadcc93871 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 @@ -2,6 +2,7 @@ package net.corda.testing.node import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import com.google.common.util.concurrent.MoreExecutors.listeningDecorator import net.corda.core.* import net.corda.core.crypto.X509Utilities import net.corda.core.crypto.appendToCommonName @@ -10,8 +11,8 @@ import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceType import net.corda.core.utilities.DUMMY_MAP import net.corda.core.utilities.WHITESPACE -import net.corda.node.driver.addressMustNotBeBound import net.corda.node.internal.Node +import net.corda.node.serialization.NodeClock import net.corda.node.services.config.ConfigHelper import net.corda.node.services.config.FullNodeConfiguration import net.corda.node.services.config.configOf @@ -21,6 +22,7 @@ import net.corda.node.utilities.ServiceIdentityGenerator import net.corda.nodeapi.User import net.corda.nodeapi.config.parseAs import net.corda.testing.MOCK_VERSION_INFO +import net.corda.testing.driver.addressMustNotBeBoundFuture import net.corda.testing.getFreeLocalPorts import org.apache.logging.log4j.Level import org.bouncycastle.asn1.x500.X500Name @@ -56,13 +58,13 @@ abstract class NodeBasedTest { */ @After fun stopAllNodes() { - val shutdownExecutor = Executors.newScheduledThreadPool(1) - nodes.forEach(Node::stop) + val shutdownExecutor = listeningDecorator(Executors.newScheduledThreadPool(nodes.size)) + Futures.allAsList(nodes.map { shutdownExecutor.submit(it::stop) }).getOrThrow() // Wait until ports are released val portNotBoundChecks = nodes.flatMap { listOf( - it.configuration.p2pAddress.let { addressMustNotBeBound(shutdownExecutor, it) }, - it.configuration.rpcAddress?.let { addressMustNotBeBound(shutdownExecutor, it) } + it.configuration.p2pAddress.let { addressMustNotBeBoundFuture(shutdownExecutor, it) }, + it.configuration.rpcAddress?.let { addressMustNotBeBoundFuture(shutdownExecutor, it) } ) }.filterNotNull() nodes.clear() @@ -117,13 +119,13 @@ abstract class NodeBasedTest { val nodeAddresses = getFreeLocalPorts("localhost", clusterSize).map { it.toString() } val masterNodeFuture = startNode( - X509Utilities.getDevX509Name("${notaryName.commonName}-0"), + X509Utilities.getX509Name("${notaryName.commonName}-0","London","demo@r3.com",null), advertisedServices = setOf(serviceInfo), configOverrides = mapOf("notaryNodeAddress" to nodeAddresses[0])) val remainingNodesFutures = (1 until clusterSize).map { startNode( - X509Utilities.getDevX509Name("${notaryName.commonName}-$it"), + X509Utilities.getX509Name("${notaryName.commonName}-$it","London","demo@r3.com",null), advertisedServices = setOf(serviceInfo), configOverrides = mapOf( "notaryNodeAddress" to nodeAddresses[it], @@ -156,7 +158,9 @@ abstract class NodeBasedTest { ) + configOverrides ) - val node = config.parseAs().createNode(MOCK_VERSION_INFO.copy(platformVersion = platformVersion)) + val parsedConfig = config.parseAs() + val node = Node(parsedConfig, parsedConfig.calculateServices(), MOCK_VERSION_INFO.copy(platformVersion = platformVersion), + if (parsedConfig.useTestClock) TestClock() else NodeClock()) node.start() nodes += node thread(name = legalName.commonName) { 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 485536deee..0104fd981c 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,10 +6,12 @@ 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.core.node.services.IdentityService 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.identity.InMemoryIdentityService import net.corda.node.services.keys.E2ETestKeyManagementService import net.corda.node.services.messaging.ArtemisMessagingServer import net.corda.node.services.messaging.NodeMessagingClient @@ -19,6 +21,7 @@ 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.cert.X509CertificateHolder import org.jetbrains.exposed.sql.Database import java.io.Closeable import java.security.KeyPair @@ -26,20 +29,23 @@ import kotlin.concurrent.thread /** * This is a bare-bones node which can only send and receive messages. It doesn't register with a network map service or - * any other such task that would make it functionable in a network and thus left to the user to do so manually. + * any other such task that would make it functional in a network and thus left to the user to do so manually. */ -class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), rpcAddress: HostAndPort = freeLocalHostAndPort()) : AutoCloseable { +class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeLocalHostAndPort(), + rpcAddress: HostAndPort = freeLocalHostAndPort(), + trustRoot: X509CertificateHolder? = null) : AutoCloseable { private val databaseWithCloseable: Pair = configureDatabase(config.dataSourceProperties) val database: Database get() = databaseWithCloseable.second val userService = RPCUserServiceImpl(config.rpcUsers) val monitoringService = MonitoringService(MetricRegistry()) val identity: KeyPair = generateKeyPair() - val keyService: KeyManagementService = E2ETestKeyManagementService(setOf(identity)) + val identityService: IdentityService = InMemoryIdentityService(trustRoot = trustRoot) + val keyService: KeyManagementService = E2ETestKeyManagementService(identityService, setOf(identity)) val executor = ServiceAffinityExecutor(config.myLegalName.commonName, 1) val broker = ArtemisMessagingServer(config, address.port, rpcAddress.port, InMemoryNetworkMapCache(), userService) val networkMapRegistrationFuture: SettableFuture = SettableFuture.create() - val net = database.transaction { + val network = database.transaction { NodeMessagingClient( config, MOCK_VERSION_INFO, @@ -53,18 +59,18 @@ class SimpleNode(val config: NodeConfiguration, val address: HostAndPort = freeL fun start() { broker.start() - net.start( + network.start( object : RPCOps { override val protocolVersion = 0 }, userService) thread(name = config.myLegalName.commonName) { - net.run(broker.serverControl) + network.run(broker.serverControl) } } override fun close() { - net.stop() + network.stop() broker.stop() databaseWithCloseable.first.close() executor.shutdownNow() diff --git a/test-utils/src/main/kotlin/net/corda/testing/performance/Injectors.kt b/test-utils/src/main/kotlin/net/corda/testing/performance/Injectors.kt new file mode 100644 index 0000000000..5690d0dba9 --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/performance/Injectors.kt @@ -0,0 +1,108 @@ +package net.corda.testing.performance + +import com.codahale.metrics.Gauge +import com.codahale.metrics.MetricRegistry +import com.google.common.base.Stopwatch +import net.corda.core.utilities.Rate +import net.corda.testing.driver.ShutdownManager +import java.time.Duration +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.Executors +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.thread +import kotlin.concurrent.withLock + +fun startTightLoopInjector( + parallelism: Int, + numberOfInjections: Int, + queueBound: Int, + work: () -> Unit +) { + ShutdownManager.run { + val executor = Executors.newFixedThreadPool(parallelism) + registerShutdown { executor.shutdown() } + val remainingLatch = CountDownLatch(numberOfInjections) + val queuedCount = AtomicInteger(0) + val lock = ReentrantLock() + val canQueueAgain = lock.newCondition() + val injector = thread(name = "injector") { + val leftToSubmit = AtomicInteger(numberOfInjections) + while (true) { + if (leftToSubmit.getAndDecrement() == 0) break + executor.submit { + work() + if (queuedCount.decrementAndGet() < queueBound / 2) { + lock.withLock { + canQueueAgain.signal() + } + } + remainingLatch.countDown() + } + if (queuedCount.incrementAndGet() > queueBound) { + lock.withLock { + canQueueAgain.await() + } + } + } + } + registerShutdown { injector.interrupt() } + remainingLatch.await() + injector.join() + } +} + +fun startPublishingFixedRateInjector( + metricRegistry: MetricRegistry, + parallelism: Int, + overallDuration: Duration, + injectionRate: Rate, + queueSizeMetricName: String = "QueueSize", + workDurationMetricName: String = "WorkDuration", + work: () -> Unit +) { + val workSemaphore = Semaphore(0) + metricRegistry.register(queueSizeMetricName, Gauge { workSemaphore.availablePermits() }) + val workDurationTimer = metricRegistry.timer(workDurationMetricName) + ShutdownManager.run { + val executor = Executors.newSingleThreadScheduledExecutor() + registerShutdown { executor.shutdown() } + val workExecutor = Executors.newFixedThreadPool(parallelism) + registerShutdown { workExecutor.shutdown() } + val timings = Collections.synchronizedList(ArrayList()) + for (i in 1..parallelism) { + workExecutor.submit { + try { + while (true) { + workSemaphore.acquire() + workDurationTimer.time { + timings.add( + Stopwatch.createStarted().apply { + work() + }.stop().elapsed(TimeUnit.MICROSECONDS) + ) + } + } + } catch (throwable: Throwable) { + throwable.printStackTrace() + } + } + } + val injector = executor.scheduleAtFixedRate( + { + workSemaphore.release((injectionRate * TimeUnit.SECONDS).toInt()) + }, + 0, + 1, + TimeUnit.SECONDS + ) + registerShutdown { + injector.cancel(true) + } + Thread.sleep(overallDuration.toMillis()) + } +} + diff --git a/test-utils/src/main/kotlin/net/corda/testing/performance/Reporter.kt b/test-utils/src/main/kotlin/net/corda/testing/performance/Reporter.kt new file mode 100644 index 0000000000..fc85aa9aed --- /dev/null +++ b/test-utils/src/main/kotlin/net/corda/testing/performance/Reporter.kt @@ -0,0 +1,34 @@ +package net.corda.testing.performance + +import com.codahale.metrics.ConsoleReporter +import com.codahale.metrics.JmxReporter +import com.codahale.metrics.MetricRegistry +import net.corda.testing.driver.ShutdownManager +import java.util.concurrent.TimeUnit +import javax.management.ObjectName +import kotlin.concurrent.thread + +fun startReporter(shutdownManager: ShutdownManager, metricRegistry: MetricRegistry = MetricRegistry()): MetricRegistry { + val jmxReporter = thread { + JmxReporter. + forRegistry(metricRegistry). + inDomain("net.corda"). + createsObjectNamesWith { _, domain, name -> + // Make the JMX hierarchy a bit better organised. + val category = name.substringBefore('.') + val subName = name.substringAfter('.', "") + if (subName == "") + ObjectName("$domain:name=$category") + else + ObjectName("$domain:type=$category,name=$subName") + }. + build(). + start() + } + shutdownManager.registerShutdown { jmxReporter.interrupt() } + val consoleReporter = thread { + ConsoleReporter.forRegistry(metricRegistry).build().start(1, TimeUnit.SECONDS) + } + shutdownManager.registerShutdown { consoleReporter.interrupt() } + return metricRegistry +} diff --git a/tools/demobench/README.md b/tools/demobench/README.md new file mode 100644 index 0000000000..dc42716039 --- /dev/null +++ b/tools/demobench/README.md @@ -0,0 +1,119 @@ +# DemoBench + +DemoBench is a standalone desktop application that makes it easy to configure +and launch local Corda nodes. Its general usage is documented +[here](https://docs.corda.net/demobench.html). + +## Running locally + +**MacOSX/Linux:** + + ./gradlew tools:demobench:installDist + cd tools/demobench/build/install/demobench + bin/demobench + +**Windows:** + + gradlew tools:demobench:installDist + cd tools\demobench\build\install\demobench + +and then + + bin\demobench + +or, if Windows complains that the command line is too long: + + java -Djava.util.logging.config.class=net.corda.demobench.config.LoggingConfig -jar lib\demobench-$version.jar + +## Testing +### The Notary Node + +When launched, DemoBench will look something like this: + +![DemoBench at launch](demobench-initial.png) + +Clicking the `Start node` button should launch a new Notary node. + +![Notary node](demobench-notary.png) + +The tab should display the correct national flag for the node's geographical +location. The `View Database`, `Launch Web Server` and `Launch Explorer` buttons +will be disabled until the node has finished booting, at which point the node +statistics (`States in vault`, `Known transactions` and `Balance`) will become +populated too. + +The Corda node should boot into a shell with a command prompt. Type `help` at +this command prompt to list the commands available, followed by `dashboard`. + +![Dashboard for Notary node](demobench-dashboard.png) + +Press `q` to exit the dashboard, and then check the tab's buttons: + +- Press `View Database` to launch the H2 database's Web console in your browser. +Pressing this button again should launch a second console session. +- Press the `Launch Web Server` button to launch the Corda Webserver for this +node. Once booted, it should open your browser to a page saying: +> ### Installed CorDaps +> No installed custom CorDapps + +- The button's text should now have changed to `Reopen web site`. Pressing the +button again should open a new session in your browser. + +- Press the `Launch Explorer` button to launch the [Node Explorer](https://docs.corda.net/node-explorer.html) for this notary. You should be logged into the +Explorer automatically. The `Launch Explorer` button should now remain disabled +until you close this node's Explorer again. + +### The Bank Node + +Click the `Add Node` button, and DemoBench will ask you to configure another +node in a new tab. + +![Configure Bank Node](demobench-configure-bank.png) + +This time, there will be additional services available. Select `corda.cash` and +`corda.issuer.GBP`, and then press the `Start node` button. + +When you press the `Launch Web Server` this time, your browser should open to a +page saying: +> ### Installed CorDapps +> **net.corda.bank.plugin.BankOfCordaPlugin**
+> net.corda.bank.api.BankOfCordaWebApi: +> - POST issue-asset-request +> - GET date + +Clicking on the `GET date` link should return today's date within a JSON document. + +Launch the bank's Node Explorer, and check the network view. The Notary node +should be displayed in Rome, whereas the Bank of Breakfast Tea should be in +Liverpool. + +## Saving / Loading profiles + +Choose `File/Save As` from DemoBench's main menu. + +![Save Profile Dialogue](demobench-save-profile.png) + +Save the profile and then examine its contents (ZIP format). It should look +something like: + +``` + Length Date Time Name +--------- ---------- ----- ---- + 0 05-25-2017 11:57 notary/ + 490 05-25-2017 11:57 notary/node.conf + 0 05-25-2017 11:57 notary/plugins/ + 0 05-25-2017 11:57 bankofbreakfasttea/ + 673 05-25-2017 11:57 bankofbreakfasttea/node.conf + 0 05-25-2017 11:57 bankofbreakfasttea/plugins/ +--------- ------- + 1163 6 files +``` + +Now choose `File/Open` from the main menu, and select the profile that you have +just saved. DemoBench should close the two existing tabs and then relaunch the +Notary and Bank nodes. + +## Exiting DemoBench + +Close DemoBench as a normal application on your platform; it should close any +open Node Explorers before exiting. diff --git a/tools/demobench/build.gradle b/tools/demobench/build.gradle index 6005379e4b..18c12a3b62 100644 --- a/tools/demobench/build.gradle +++ b/tools/demobench/build.gradle @@ -1,7 +1,7 @@ buildscript { ext.tornadofx_version = '1.7.3' ext.jna_version = '4.1.0' - ext.purejavacomm_version = '0.0.17' + ext.purejavacomm_version = '0.0.18' ext.controlsfx_version = '8.40.12' ext.java_home = System.properties.'java.home' @@ -9,6 +9,7 @@ buildscript { ext.pkg_outDir = "$buildDir/javapackage" ext.dist_source = "$pkg_source/demobench-$version" ext.pkg_version = "$version".indexOf('-') >= 0 ? "$version".substring(0, "$version".indexOf('-')) : version + ext.pkg_macosxKeyUserName = 'R3CEV' repositories { mavenLocal() @@ -61,12 +62,8 @@ dependencies { // FontAwesomeFX: icons in the form of a font. compile "de.jensd:fontawesomefx-fontawesome:4.7.0-5" - // These libraries don't exist in any Maven repository I can find. - // See: https://github.com/JetBrains/jediterm - // - // The terminal JAR here has also been tweaked: - // See: https://github.com/JetBrains/jediterm/issues/144 - compile ':jediterm-terminal-2.5' + // JediTerm: the terminal emulator used in IntelliJ. We have forked it and tweaked it, see https://github.com/corda/jediterm + compile ':terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1' compile ':pty4j-0.7.2' testCompile project(':node') @@ -160,8 +157,9 @@ task javapackage(dependsOn: distZip) { include '**/*.wsf' include '**/*.manifest' } - filter { - line -> line.replaceAll('@pkg_version@', pkg_version) + filter { line -> + line.replaceAll('@pkg_version@', pkg_version) + .replaceAll('@signingKeyUserName@', pkg_macosxKeyUserName) } into "$pkg_source/package" } @@ -193,6 +191,9 @@ task javapackage(dependsOn: distZip) { } } + // This is specific to MacOSX packager. + bundleArgument(arg: 'mac.signing-key-user-name', value: pkg_macosxKeyUserName) + platform { property(name: 'java.util.logging.config.class', value: 'net.corda.demobench.config.LoggingConfig') property(name: 'org.jboss.logging.provider', value: 'slf4j') diff --git a/tools/demobench/demobench-configure-bank.png b/tools/demobench/demobench-configure-bank.png new file mode 100644 index 0000000000..5e2c01211b Binary files /dev/null and b/tools/demobench/demobench-configure-bank.png differ diff --git a/tools/demobench/demobench-dashboard.png b/tools/demobench/demobench-dashboard.png new file mode 100644 index 0000000000..1ef05ba361 Binary files /dev/null and b/tools/demobench/demobench-dashboard.png differ diff --git a/tools/demobench/demobench-initial.png b/tools/demobench/demobench-initial.png new file mode 100644 index 0000000000..27c2855e2c Binary files /dev/null and b/tools/demobench/demobench-initial.png differ diff --git a/tools/demobench/demobench-notary.png b/tools/demobench/demobench-notary.png new file mode 100644 index 0000000000..519df2fac1 Binary files /dev/null and b/tools/demobench/demobench-notary.png differ diff --git a/tools/demobench/demobench-save-profile.png b/tools/demobench/demobench-save-profile.png new file mode 100644 index 0000000000..c03c278745 Binary files /dev/null and b/tools/demobench/demobench-save-profile.png differ diff --git a/tools/demobench/libs/jediterm-terminal-2.5.jar b/tools/demobench/libs/terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1.jar similarity index 55% rename from tools/demobench/libs/jediterm-terminal-2.5.jar rename to tools/demobench/libs/terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1.jar index b211b2d08e..71bc563330 100644 Binary files a/tools/demobench/libs/jediterm-terminal-2.5.jar and b/tools/demobench/libs/terminal-331a005d6793e52cefc9e2cec6774e62d5a546b1.jar differ diff --git a/tools/demobench/package/macosx/Corda DemoBench-post-image.sh b/tools/demobench/package/macosx/Corda DemoBench-post-image.sh index c38febc5f0..2d1c42fd2f 100644 --- a/tools/demobench/package/macosx/Corda DemoBench-post-image.sh +++ b/tools/demobench/package/macosx/Corda DemoBench-post-image.sh @@ -4,13 +4,13 @@ if [ -z "$JAVA_HOME" ]; then fi function signApplication() { - APPDIR=$1 - IDENTITY=$2 + APPDIR="$1" + IDENTITY="$2" - # Resign the embedded JRE because we have included "bin/java" + # Re-sign the embedded JRE because we have included "bin/java" # after javapackager had already signed the JRE installation. - if ! (codesign --force --sign "$IDENTITY" --verbose "$APPDIR/Contents/PlugIns/Java.runtime"); then - echo "**** Failed to resign the embedded JVM" + if ! (codesign --force --sign "$IDENTITY" --preserve-metadata=identifier,entitlements,requirements --verbose "$APPDIR/Contents/PlugIns/Java.runtime"); then + echo "**** Failed to re-sign the embedded JVM" return 1 fi } @@ -27,7 +27,7 @@ fi cd .. # Sign the application using a 'Developer ID Application' key on our keychain. -if ! (signApplication "Corda DemoBench.app" "Developer ID Application: "); then +if ! (signApplication "Corda DemoBench.app" "Developer ID Application: @signingKeyUserName@"); then echo "**** Failed to sign the application - ABORT SIGNING" fi diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt index 8d4318e47f..52f76905be 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/explorer/Explorer.kt @@ -11,6 +11,7 @@ import net.corda.demobench.readErrorLines import tornadofx.* import java.io.IOException import java.nio.file.Files +import java.nio.file.StandardCopyOption.* import java.util.concurrent.Executors class Explorer internal constructor(private val explorerController: ExplorerController) : AutoCloseable { @@ -89,13 +90,13 @@ class Explorer internal constructor(private val explorerController: ExplorerCont Files.createSymbolicLink(destPath, path) } catch(e: UnsupportedOperationException) { // OS doesn't support symbolic links? - Files.copy(path, destPath) - } catch(e: FileAlreadyExistsException) { + Files.copy(path, destPath, REPLACE_EXISTING) + } catch (e: java.nio.file.FileAlreadyExistsException) { // OK, don't care ... } catch (e: IOException) { // Windows 10 might not allow this user to create a symlink log.warn("Failed to create symlink '{}' for '{}': {}", destPath, path, e.message) - Files.copy(path, destPath) + Files.copy(path, destPath, REPLACE_EXISTING) } } } diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt index 9a10e04652..c546657e7b 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/InstallFactory.kt @@ -3,7 +3,6 @@ package net.corda.demobench.model import com.google.common.net.HostAndPort import com.typesafe.config.Config import org.bouncycastle.asn1.x500.X500Name - import tornadofx.* import java.io.IOException import java.nio.file.Files @@ -20,12 +19,13 @@ class InstallFactory : Controller() { val rpcPort = config.parsePort("rpcAddress") val webPort = config.parsePort("webAddress") val h2Port = config.getInt("h2port") + val x500name = X500Name(config.getString("myLegalName")) val extraServices = config.parseExtraServices("extraAdvertisedServiceIds") val tempDir = Files.createTempDirectory(baseDir, ".node") val nodeConfig = NodeConfig( tempDir, - X500Name(config.getString("myLegalName")), + x500name, p2pPort, rpcPort, webPort, diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt index 3e4c3d1652..148cc9025b 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/JVMConfig.kt @@ -3,14 +3,15 @@ package net.corda.demobench.model import javafx.scene.control.Alert import javafx.scene.control.Alert.AlertType.ERROR import javafx.stage.Stage -import tornadofx.* import java.nio.file.Path import java.nio.file.Paths +import tornadofx.* class JVMConfig : Controller() { val userHome: Path = Paths.get(System.getProperty("user.home")).toAbsolutePath() val dataHome: Path = userHome.resolve("demobench") + val capsuleHome: Path = dataHome.resolve(".capsule") val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java") val applicationDir: Path = Paths.get(System.getProperty("user.dir")).toAbsolutePath() @@ -23,9 +24,12 @@ class JVMConfig : Controller() { } fun processFor(jarPath: Path, vararg args: String): ProcessBuilder { - return ProcessBuilder(commandFor(jarPath, *args)) + return ProcessBuilder(commandFor(jarPath, *args)).apply { setCapsuleCacheDir(environment()) } } + fun setCapsuleCacheDir(env: MutableMap): MutableMap = env.apply { + put("CAPSULE_CACHE_DIR", capsuleHome.toString()) + } } typealias atRuntime = (Path, String) -> Unit 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 7a73d40e28..14ce84779f 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 @@ -1,7 +1,7 @@ package net.corda.demobench.model import com.typesafe.config.* -import net.corda.core.crypto.location +import net.corda.core.crypto.locationOrNull import net.corda.nodeapi.User import org.bouncycastle.asn1.x500.X500Name import java.io.File @@ -26,7 +26,7 @@ class NodeConfig( val defaultUser = user("guest") } - val nearestCity: String? = legalName.location + val nearestCity: String = legalName.locationOrNull ?: "Unknown location" val nodeDir: Path = baseDir.resolve(key) override val pluginDir: Path = nodeDir.resolve("plugins") val explorerDir: Path = baseDir.resolve("$key-explorer") @@ -47,10 +47,9 @@ class NodeConfig( .withValue("myLegalName", valueFor(legalName.toString())) .withValue("p2pAddress", addressValueFor(p2pPort)) .withValue("extraAdvertisedServiceIds", valueFor(extraServices)) - .withFallback(optional("networkMapService", networkMap, { - c, n -> + .withFallback(optional("networkMapService", networkMap, { c, n -> c.withValue("address", addressValueFor(n.p2pPort)) - .withValue("legalName", valueFor(n.legalName.toString())) + .withValue("legalName", valueFor(n.legalName.toString())) })) .withValue("webAddress", addressValueFor(webPort)) .withValue("rpcAddress", addressValueFor(rpcPort)) 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 31460611d9..b593bc32ce 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 @@ -116,7 +116,10 @@ class NodeController(check: atRuntime = ::checkExists) : Controller() { confFile.writeText(config.toText()) // Execute the Corda node - pty.run(command, System.getenv(), nodeDir.toString()) + val cordaEnv = System.getenv().toMutableMap().apply { + jvm.setCapsuleCacheDir(this) + } + pty.run(command, cordaEnv, nodeDir.toString()) log.info("Launched node: ${config.legalName}") return true } catch (e: Exception) { diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt index 0138ae92d2..e4f234fa10 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/NodeData.kt @@ -19,7 +19,8 @@ object SuggestedDetails { "Bank of Big Apples" to "New York", "Bank of Baguettes" to "Paris", "Bank of Fondue" to "Geneve", - "Bank of Maple Syrup" to "Toronto" + "Bank of Maple Syrup" to "Toronto", + "Bank of Golden Gates" to "San Francisco" ) private var cursor = 0 diff --git a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt index 0974a4f6df..cfd1c23038 100644 --- a/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt +++ b/tools/demobench/src/main/kotlin/net/corda/demobench/model/ServiceController.kt @@ -4,7 +4,6 @@ import tornadofx.* import java.io.IOException import java.io.InputStreamReader import java.net.URL -import java.util.* import java.util.logging.Level class ServiceController(resourceName: String = "/services.conf") : Controller() { @@ -17,11 +16,11 @@ class ServiceController(resourceName: String = "/services.conf") : Controller() * Load our list of known extra Corda services. */ private fun loadConf(url: URL?): List { - if (url == null) { - return emptyList() + return if (url == null) { + emptyList() } else { try { - val set = TreeSet() + val set = sortedSetOf() InputStreamReader(url.openStream()).useLines { sq -> sq.forEach { line -> val service = line.trim() @@ -30,12 +29,11 @@ class ServiceController(resourceName: String = "/services.conf") : Controller() log.info("Supports: $service") } } - return set.toList() + set.toList() } catch (e: IOException) { log.log(Level.SEVERE, "Failed to load $url: ${e.message}", e) - return emptyList() + emptyList() } } } - } 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 68152fbfc1..734aec73b4 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 @@ -44,14 +44,13 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension @Throws(IOException::class) fun run(args: Array, envs: Map, workingDir: String?) { - check(!terminal.isSessionRunning, { "${terminal.sessionName} is already running" }) + check(!terminal.isSessionRunning) { "${terminal.sessionName} is already running" } val environment = envs.toMutableMap() if (!UIUtil.isWindows) { environment["TERM"] = "xterm" - - // This environment variable is specific to MacOSX. - environment.remove("TERM_PROGRAM") + // This, in combination with running on a Mac JetBrains JRE, enables emoji in Mac demobench. + environment["TERM_PROGRAM"] = "JediTerm" } val connector = createTtyConnector(args, environment, workingDir) @@ -64,8 +63,7 @@ class R3Pty(val name: X500Name, settings: SettingsProvider, dimension: Dimension onExit(exitValue) } - val session = terminal.createTerminalSession(connector) - session.start() + terminal.createTerminalSession(connector).apply { start() } } @Suppress("unused") 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 44a62537cb..d63c7ff366 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 @@ -263,7 +263,7 @@ class NodeTabView : Fragment() { } private fun launchNode(config: NodeConfig) { - val countryCode = CityDatabase.cityMap[config.nearestCity ?: "Nowhere"]?.countryCode + val countryCode = CityDatabase.cityMap[config.nearestCity]?.countryCode if (countryCode != null) { nodeTab.graphic = ImageView(flags.get()[countryCode]).apply { fitWidth = 24.0; isPreserveRatio = true } } diff --git a/tools/demobench/src/main/resources/services.conf b/tools/demobench/src/main/resources/services.conf index 64a19b59d0..cb475c0ade 100644 --- a/tools/demobench/src/main/resources/services.conf +++ b/tools/demobench/src/main/resources/services.conf @@ -4,4 +4,5 @@ corda.interest_rates corda.issuer.USD corda.issuer.GBP corda.issuer.CHF +corda.issuer.EUR corda.cash diff --git a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt index 0bcb8689be..e5db3e1233 100644 --- a/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt +++ b/tools/demobench/src/test/kotlin/net/corda/demobench/model/NodeConfigTest.kt @@ -249,7 +249,7 @@ class NodeConfigTest { } private fun createConfig( - legalName: X500Name = X500Name("CN=Unknown,O=R3,OU=corda,L=Nowhere,C=UK"), + legalName: X500Name = X500Name("CN=Unknown,O=R3,OU=corda,L=Nowhere,C=GB"), p2pPort: Int = -1, rpcPort: Int = -1, webPort: Int = -1, diff --git a/tools/explorer/build.gradle b/tools/explorer/build.gradle index f9502fd833..3fac788e92 100644 --- a/tools/explorer/build.gradle +++ b/tools/explorer/build.gradle @@ -30,7 +30,7 @@ dependencies { compile project(':core') compile project(':client:jfx') compile project(':client:mock') - compile project(':node') + compile project(':test-utils') compile project(':finance') // Capsule is a library for building independently executable fat JARs. diff --git a/tools/explorer/capsule/build.gradle b/tools/explorer/capsule/build.gradle index 9c4c929e1a..c0a35f7b20 100644 --- a/tools/explorer/capsule/build.gradle +++ b/tools/explorer/capsule/build.gradle @@ -29,7 +29,7 @@ task buildExplorerJAR(type: FatCapsule, dependsOn: project(':tools:explorer').co applicationClass 'net.corda.explorer.Main' archiveName "node-explorer-${corda_release_version}.jar" applicationSource = files( - project(':tools:explorer').configurations.compile, + project(':tools:explorer').configurations.runtime, project(':tools:explorer').jar, '../build/classes/main/ExplorerCaplet.class' ) diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt index ebb4d5b34b..9a7ada66ea 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/ExplorerSimulation.kt @@ -27,9 +27,9 @@ import net.corda.flows.CashFlowCommand import net.corda.flows.CashIssueFlow import net.corda.flows.CashPaymentFlow import net.corda.flows.IssuerFlow -import net.corda.node.driver.NodeHandle -import net.corda.node.driver.PortAllocation -import net.corda.node.driver.driver +import net.corda.testing.driver.NodeHandle +import net.corda.testing.driver.PortAllocation +import net.corda.testing.driver.driver import net.corda.node.services.startFlowPermission import net.corda.node.services.transactions.SimpleNotaryService import net.corda.nodeapi.User @@ -79,7 +79,7 @@ class ExplorerSimulation(val options: OptionSet) { val bob = startNode(BOB.name, rpcUsers = arrayListOf(user), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("cash"))), customOverrides = mapOf("nearestCity" to "Madrid")) - val ukBankName = X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=UK") + val ukBankName = X500Name("CN=UK Bank Plc,O=UK Bank Plc,L=London,C=GB") val usaBankName = X500Name("CN=USA Bank Corp,O=USA Bank Corp,L=New York,C=USA") val issuerGBP = startNode(ukBankName, rpcUsers = arrayListOf(manager), advertisedServices = setOf(ServiceInfo(ServiceType.corda.getSubType("issuer.GBP"))), diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt index 0e05429da6..31dafe684e 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/IssuerModel.kt @@ -10,7 +10,7 @@ import net.corda.core.contracts.currency import net.corda.core.node.NodeInfo import tornadofx.* -val ISSUER_SERVICE_TYPE = Regex("corda.issuer.(USD|GBP|CHF)") +val ISSUER_SERVICE_TYPE = Regex("corda.issuer.(USD|GBP|CHF|EUR)") class IssuerModel { private val networkIdentities by observableList(NetworkIdentityModel::networkIdentities) @@ -37,4 +37,4 @@ class IssuerModel { currency(issuer.info.type.id.substringAfterLast(".")) } else null -} \ No newline at end of file +} diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt index 76148b1393..3e1bf5a917 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/model/ReportingCurrencyModel.kt @@ -5,10 +5,7 @@ import net.corda.client.jfx.model.ExchangeRate import net.corda.client.jfx.model.ExchangeRateModel import net.corda.client.jfx.model.observableValue import net.corda.client.jfx.utils.AmountBindings -import net.corda.core.contracts.Amount -import net.corda.core.contracts.CHF -import net.corda.core.contracts.GBP -import net.corda.core.contracts.USD +import net.corda.core.contracts.* import org.fxmisc.easybind.EasyBind import tornadofx.* import java.util.* @@ -16,7 +13,7 @@ import java.util.* class ReportingCurrencyModel { private val exchangeRate: ObservableValue by observableValue(ExchangeRateModel::exchangeRate) val reportingCurrency by observableValue(SettingsModel::reportingCurrencyProperty) - val supportedCurrencies = setOf(USD, GBP, CHF).toList().observable() + val supportedCurrencies = setOf(USD, GBP, CHF, EUR).toList().observable() /** * This stream provides a stream of exchange() functions that updates when either the reporting currency or the 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 deleted file mode 100644 index 0dbc6b5e4e..0000000000 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/plugin/ExplorerPlugin.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.corda.explorer.plugin - -import net.corda.core.identity.Party -import net.corda.core.node.CordaPluginRegistry -import net.corda.flows.IssuerFlow -import java.util.function.Function - -class ExplorerPlugin : CordaPluginRegistry() { - override val servicePlugins = listOf(Function(IssuerFlow.Issuer::Service)) -} diff --git a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt index 81251c9891..e0883cc311 100644 --- a/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt +++ b/tools/explorer/src/main/kotlin/net/corda/explorer/views/cordapps/cash/NewTransaction.kt @@ -183,7 +183,7 @@ class NewTransaction : Fragment() { // Issuer issuerLabel.visibleProperty().bind(transactionTypeCB.valueProperty().isNotNull) issuerChoiceBox.apply { - items = issuers.map { it.legalIdentity }.unique().sorted() + items = issuers.map { it.legalIdentity as Party }.unique().sorted() converter = stringConverter { PartyNameFormatter.short.format(it.name) } visibleProperty().bind(transactionTypeCB.valueProperty().map { it == CashTransaction.Pay }) } diff --git a/tools/explorer/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry b/tools/explorer/src/main/resources/META-INF/services/net.corda.core.node.CordaPluginRegistry deleted file mode 100644 index bb2442e787..0000000000 --- a/tools/explorer/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.explorer.plugin.ExplorerPlugin diff --git a/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css b/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css index c9d5933b3d..d716dbd60b 100644 --- a/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css +++ b/tools/explorer/src/main/resources/net/corda/explorer/css/corda.css @@ -273,6 +273,45 @@ -fx-stroke: red; } +.scroll-bar:horizontal .track, +.scroll-bar:vertical .track { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-background-radius: 0em; + -fx-border-radius: 2em; +} + +.scroll-bar:horizontal .increment-button, +.scroll-bar:horizontal .decrement-button { + -fx-background-color: transparent; + -fx-background-radius: 0em; + -fx-padding: 0 0 10 0; +} + +.scroll-bar:vertical .increment-button, +.scroll-bar:vertical .decrement-button { + -fx-background-color: transparent; + -fx-background-radius: 0em; + -fx-padding: 0 10 0 0; +} + +.scroll-bar .increment-arrow, +.scroll-bar .decrement-arrow { + -fx-shape: " "; + -fx-padding: 0; +} + +.scroll-bar:horizontal .thumb, +.scroll-bar:vertical .thumb { + -fx-background-color: derive(black, 90%); + -fx-background-insets: 2, 0, 0; + -fx-background-radius: 2em; +} + +.scroll-bar:vertical { + -fx-background-color: transparent; +} + /* Other */ .identicon { -fx-border-radius: 2; diff --git a/tools/loadtest/build.gradle b/tools/loadtest/build.gradle index 0539d7e2de..68a5a50774 100644 --- a/tools/loadtest/build.gradle +++ b/tools/loadtest/build.gradle @@ -6,7 +6,7 @@ mainClassName = 'net.corda.loadtest.MainKt' dependencies { compile project(':client:mock') compile project(':client:rpc') - compile project(':node') + compile project(':test-utils') // https://mvnrepository.com/artifact/com.jcraft/jsch compile group: 'com.jcraft', name: 'jsch', version: '0.1.54' @@ -25,4 +25,13 @@ run { if (project.hasProperty('loadtest-config')) { args project["loadtest-config"] } + System.properties.forEach { k, v -> + if (k.toString().startsWith("loadtest.")) + systemProperty k, v + } + if (System.properties.getProperty('consoleLogLevel') != null) { + logging.captureStandardOutput(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel'))) + logging.captureStandardError(LogLevel.valueOf(System.properties.getProperty('consoleLogLevel'))) + systemProperty "consoleLogLevel", System.properties.getProperty('consoleLogLevel') + } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt index bf46ebff5b..ef84237cbb 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/ConnectionManager.kt @@ -1,17 +1,15 @@ package net.corda.loadtest import com.google.common.net.HostAndPort -import com.jcraft.jsch.* +import com.jcraft.jsch.Buffer +import com.jcraft.jsch.Identity +import com.jcraft.jsch.IdentityRepository +import com.jcraft.jsch.JSch import com.jcraft.jsch.agentproxy.AgentProxy import com.jcraft.jsch.agentproxy.connector.SSHAgentConnector import com.jcraft.jsch.agentproxy.usocket.JNAUSocketFactory -import net.corda.client.rpc.CordaRPCClient -import net.corda.client.rpc.CordaRPCConnection -import net.corda.core.messaging.CordaRPCOps -import net.corda.node.driver.PortAllocation +import net.corda.testing.driver.PortAllocation import org.slf4j.LoggerFactory -import java.io.ByteArrayOutputStream -import java.io.Closeable import java.util.* import kotlin.streams.toList @@ -62,27 +60,23 @@ fun setupJSchWithSshAgent(): JSch { } } -class ConnectionManager(private val username: String, private val jSch: JSch) { - fun connectToNode( - nodeHost: String, - remoteMessagingPort: Int, - localTunnelAddress: HostAndPort, - rpcUsername: String, - rpcPassword: String - ): NodeConnection { - val session = jSch.getSession(username, nodeHost, 22) +class ConnectionManager(private val jSch: JSch) { + fun connectToNode(remoteNode: RemoteNode, localTunnelAddress: HostAndPort): NodeConnection { + val session = jSch.getSession(remoteNode.sshUserName, remoteNode.hostname, 22) // We don't check the host fingerprints because they may change often session.setConfig("StrictHostKeyChecking", "no") - log.info("Connecting to $nodeHost...") + log.info("Connecting to ${remoteNode.hostname}...") session.connect() - log.info("Connected to $nodeHost!") + log.info("Connected to ${remoteNode.hostname}!") - log.info("Creating tunnel from $nodeHost:$remoteMessagingPort to $localTunnelAddress...") - session.setPortForwardingL(localTunnelAddress.port, localTunnelAddress.host, remoteMessagingPort) + log.info("Creating tunnel from ${remoteNode.hostname} to $localTunnelAddress...") + session.setPortForwardingL(localTunnelAddress.port, localTunnelAddress.host, remoteNode.rpcPort) log.info("Tunnel created!") - val connection = NodeConnection(nodeHost, session, localTunnelAddress, rpcUsername, rpcPassword) - connection.startClient() + val connection = NodeConnection(remoteNode, session, localTunnelAddress) + connection.startNode() + connection.waitUntilUp() + connection.startRPCClient() return connection } } @@ -98,26 +92,11 @@ class ConnectionManager(private val username: String, private val jSch: JSch) { * @param withConnections An action to run once we're connected to the nodes. * @return The return value of [withConnections] */ -fun
connectToNodes( - username: String, - nodeHosts: List, - remoteMessagingPort: Int, - tunnelPortAllocation: PortAllocation, - rpcUsername: String, - rpcPassword: String, - withConnections: (List) -> A -): A { - val manager = ConnectionManager(username, setupJSchWithSshAgent()) - val connections = nodeHosts.parallelStream().map { nodeHost -> - manager.connectToNode( - nodeHost = nodeHost, - remoteMessagingPort = remoteMessagingPort, - localTunnelAddress = tunnelPortAllocation.nextHostAndPort(), - rpcUsername = rpcUsername, - rpcPassword = rpcPassword - ) +fun connectToNodes(remoteNodes: List, tunnelPortAllocation: PortAllocation, withConnections: (List) -> A): A { + val manager = ConnectionManager(setupJSchWithSshAgent()) + val connections = remoteNodes.parallelStream().map { remoteNode -> + manager.connectToNode(remoteNode, tunnelPortAllocation.nextHostAndPort()) }.toList() - return try { withConnections(connections) } finally { @@ -125,108 +104,6 @@ fun connectToNodes( } } -/** - * [NodeConnection] allows executing remote shell commands on the node as well as executing RPCs. - * The RPC Client start/stop must be controlled externally with [startClient] and [doWhileClientStopped]. For example - * if we want to do some action on the node that requires bringing down of the node we should nest it in a - * [doWhileClientStopped], otherwise the RPC link will be broken. - */ -class NodeConnection( - val hostName: String, - private val jSchSession: Session, - private val localTunnelAddress: HostAndPort, - private val rpcUsername: String, - private val rpcPassword: String -) : Closeable { - private val client = CordaRPCClient(localTunnelAddress) - private var connection: CordaRPCConnection? = null - val proxy: CordaRPCOps get() = connection?.proxy ?: throw IllegalStateException("proxy requested, but the client is not running") - - data class ShellCommandOutput( - val originalShellCommand: String, - val exitCode: Int, - val stdout: String, - val stderr: String - ) { - fun getResultOrThrow(): String { - if (exitCode != 0) { - val diagnostic = - "There was a problem running \"$originalShellCommand\":\n" + - " stdout:\n$stdout" + - " stderr:\n$stderr" - log.error(diagnostic) - throw Exception(diagnostic) - } else { - return stdout - } - } - } - - fun doWhileClientStopped(action: () -> A): A { - val connection = connection - require(connection != null) { "doWhileClientStopped called with no running client" } - log.info("Stopping RPC proxy to $hostName, tunnel at $localTunnelAddress") - connection!!.close() - try { - return action() - } finally { - log.info("Starting new RPC proxy to $hostName, tunnel at $localTunnelAddress") - // TODO expose these somehow? - val newConnection = client.start(rpcUsername, rpcPassword) - this.connection = newConnection - } - } - - fun startClient() { - log.info("Creating RPC proxy to $hostName, tunnel at $localTunnelAddress") - connection = client.start(rpcUsername, rpcPassword) - log.info("Proxy created") - } - - /** - * @return Pair of (stdout, stderr) of command - */ - fun runShellCommandGetOutput(command: String): ShellCommandOutput { - log.info("Running '$command' on $hostName") - val (exitCode, pair) = withChannelExec(command) { channel -> - val stdoutStream = ByteArrayOutputStream() - val stderrStream = ByteArrayOutputStream() - channel.outputStream = stdoutStream - channel.setErrStream(stderrStream) - channel.connect() - poll { channel.isEOF } - Pair(stdoutStream.toString(), stderrStream.toString()) - } - return ShellCommandOutput( - originalShellCommand = command, - exitCode = exitCode, - stdout = pair.first, - stderr = pair.second - ) - } - - /** - * @param function should call [ChannelExec.connect] - * @return A pair of (exit code, [function] return value) - */ - private fun withChannelExec(command: String, function: (ChannelExec) -> A): Pair { - val channel = jSchSession.openChannel("exec") as ChannelExec - channel.setCommand(command) - try { - val result = function(channel) - poll { channel.isEOF } - return Pair(channel.exitStatus, result) - } finally { - channel.disconnect() - } - } - - override fun close() { - connection?.close() - jSchSession.disconnect() - } -} - fun poll(intervalMilliseconds: Long = 500, function: () -> Boolean) { while (!function()) { Thread.sleep(intervalMilliseconds) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt index 74a221b9e4..4db3fa1863 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Disruption.kt @@ -16,11 +16,11 @@ private val log = LoggerFactory.getLogger(Disruption::class.java) // DOCS START 1 data class Disruption( val name: String, - val disrupt: (NodeHandle, SplittableRandom) -> Unit + val disrupt: (NodeConnection, SplittableRandom) -> Unit ) data class DisruptionSpec( - val nodeFilter: (NodeHandle) -> Boolean, + val nodeFilter: (NodeConnection) -> Boolean, val disruption: Disruption, val noDisruptionWindowMs: LongRange ) @@ -43,8 +43,8 @@ data class DisruptionSpec( * * Randomly duplicate messages, perhaps to other queues even. */ -val isNetworkMap = { node: NodeHandle -> node.info.advertisedServices.any { it.info.type == NetworkMapService.type } } -val isNotary = { node: NodeHandle -> node.info.advertisedServices.any { it.info.type.isNotary() } } +val isNetworkMap = { node: NodeConnection -> node.info.advertisedServices.any { it.info.type == NetworkMapService.type } } +val isNotary = { node: NodeConnection -> node.info.advertisedServices.any { it.info.type.isNotary() } } fun ((A) -> Boolean).or(other: (A) -> Boolean): (A) -> Boolean = { this(it) || other(it) } fun hang(hangIntervalRange: LongRange) = Disruption("Hang randomly") { node, random -> @@ -52,21 +52,21 @@ fun hang(hangIntervalRange: LongRange) = Disruption("Hang randomly") { node, ran node.doWhileSigStopped { Thread.sleep(hangIntervalMs) } } -val restart = Disruption("Restart randomly") { (configuration, connection), _ -> - connection.runShellCommandGetOutput("sudo systemctl restart ${configuration.remoteSystemdServiceName}").getResultOrThrow() +val restart = Disruption("Restart randomly") { connection, _ -> + connection.restartNode() + connection.waitUntilUp() } val kill = Disruption("Kill randomly") { node, _ -> - val pid = node.getNodePid() - node.connection.runShellCommandGetOutput("sudo kill $pid") + node.kill() } -val deleteDb = Disruption("Delete persistence database without restart") { (configuration, connection), _ -> - connection.runShellCommandGetOutput("sudo rm ${configuration.remoteNodeDirectory}/persistence.mv.db").getResultOrThrow() +val deleteDb = Disruption("Delete persistence database without restart") { connection, _ -> + connection.runShellCommandGetOutput("sudo rm ${connection.remoteNode.nodeDirectory}/persistence.mv.db").getResultOrThrow() } // DOCS START 2 -fun strainCpu(parallelism: Int, durationSeconds: Int) = Disruption("Put strain on cpu") { (_, connection), _ -> +fun strainCpu(parallelism: Int, durationSeconds: Int) = Disruption("Put strain on cpu") { connection, _ -> val shell = "for c in {1..$parallelism} ; do openssl enc -aes-128-cbc -in /dev/urandom -pass pass: -e > /dev/null & done && JOBS=\$(jobs -p) && (sleep $durationSeconds && kill \$JOBS) & wait" connection.runShellCommandGetOutput(shell).getResultOrThrow() } @@ -90,7 +90,7 @@ fun Nodes.withDisruptions(disruptions: List, mainRandom: Spl executor.invokeAll(nodes.map { node -> val nodeRandom = random.split() Callable { - log.info("Disrupting ${node.connection.hostName} with '${disruption.disruption.name}'") + log.info("Disrupting ${node.remoteNode.hostname} with '${disruption.disruption.name}'") disruption.disruption.disrupt(node, nodeRandom) } }) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt index 9c96f46f70..34c9972798 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTest.kt @@ -1,14 +1,16 @@ package net.corda.loadtest +import com.google.common.util.concurrent.RateLimiter import net.corda.client.mock.Generator import net.corda.client.rpc.notUsed import net.corda.core.crypto.toBase58String -import net.corda.node.driver.PortAllocation import net.corda.node.services.network.NetworkMapService +import net.corda.testing.driver.PortAllocation import org.slf4j.LoggerFactory import java.util.* +import java.util.concurrent.Callable import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.Executors private val log = LoggerFactory.getLogger(LoadTest::class.java) @@ -61,6 +63,7 @@ data class LoadTest( val parallelism: Int, val generateCount: Int, val clearDatabaseBeforeRun: Boolean, + val executionFrequency: Int?, val gatherFrequency: Int, val disruptionPatterns: List> ) @@ -77,12 +80,19 @@ data class LoadTest( } } + val rateLimiter = parameters.executionFrequency?.let { + log.info("Execution rate limited to $it per second.") + RateLimiter.create(it.toDouble()) + } + val executor = Executors.newFixedThreadPool(parameters.parallelism) + parameters.disruptionPatterns.forEach { disruptions -> log.info("Running test '$testName' with disruptions ${disruptions.map { it.disruption.name }}") nodes.withDisruptions(disruptions, random) { var state = nodes.gatherRemoteState(null) var count = parameters.generateCount var countSinceLastCheck = 0 + while (count > 0) { log.info("$count remaining commands, state:\n$state") // Generate commands @@ -92,21 +102,21 @@ data class LoadTest( // Interpret commands val newState = commands.fold(state, interpret) // Execute commands - val queue = ConcurrentLinkedQueue(commands) - (1..parameters.parallelism).toList().parallelStream().forEach { - var next = queue.poll() - while (next != null) { - log.info("Executing $next") - try { - nodes.execute(next) - next = queue.poll() - } catch (exception: Throwable) { - val diagnostic = executeDiagnostic(state, newState, next, exception) - log.error(diagnostic) - throw Exception(diagnostic) + executor.invokeAll( + commands.map { + Callable { + rateLimiter?.acquire() + log.info("Executing $it") + try { + nodes.execute(it) + } catch (exception: Throwable) { + val diagnostic = executeDiagnostic(state, newState, it, exception) + log.error(diagnostic) + throw Exception(diagnostic) + } + } } - } - } + ) countSinceLastCheck += commands.size if (countSinceLastCheck >= parameters.gatherFrequency) { log.info("Checking consistency...") @@ -129,7 +139,7 @@ data class LoadTest( log.info("'$testName' done!") } } - + executor.shutdown() } companion object { @@ -143,9 +153,9 @@ data class LoadTest( } data class Nodes( - val notary: NodeHandle, - val networkMap: NodeHandle, - val simpleNodes: List + val notary: NodeConnection, + val networkMap: NodeConnection, + val simpleNodes: List ) { val allNodes by lazy { (listOf(notary, networkMap) + simpleNodes).associateBy { it.info }.values } } @@ -157,53 +167,44 @@ fun runLoadTests(configuration: LoadTestConfiguration, tests: List + + val remoteNodes = configuration.nodeHosts.map { hostname -> + configuration.let { + RemoteNode(hostname, it.remoteSystemdServiceName, it.sshUser, it.rpcUser, it.rpcPort, it.remoteNodeDirectory) + } + } + + connectToNodes(remoteNodes, PortAllocation.Incremental(configuration.localTunnelStartingPort)) { connections -> log.info("Connected to all nodes!") - val hostNodeHandleMap = ConcurrentHashMap() + val hostNodeMap = ConcurrentHashMap() connections.parallelStream().forEach { connection -> - log.info("Getting node info of ${connection.hostName}") - val nodeInfo = connection.proxy.nodeIdentity() - log.info("Got node info of ${connection.hostName}: $nodeInfo!") - val (otherNodeInfos, nodeInfoUpdates) = connection.proxy.networkMapUpdates() - nodeInfoUpdates.notUsed() - val pubkeysString = otherNodeInfos.map { + log.info("Getting node info of ${connection.remoteNode.hostname}") + val info = connection.info + log.info("Got node info of ${connection.remoteNode.hostname}: $info!") + val (otherInfo, infoUpdates) = connection.proxy.networkMapUpdates() + infoUpdates.notUsed() + val pubKeysString = otherInfo.map { " ${it.legalIdentity.name}: ${it.legalIdentity.owningKey.toBase58String()}" }.joinToString("\n") - log.info("${connection.hostName} waiting for network map") + log.info("${connection.remoteNode.hostname} waiting for network map") connection.proxy.waitUntilRegisteredWithNetworkMap().get() - log.info("${connection.hostName} sees\n$pubkeysString") - val nodeHandle = NodeHandle(configuration, connection, nodeInfo) - nodeHandle.waitUntilUp() - hostNodeHandleMap.put(connection.hostName, nodeHandle) - } - - val networkMapNode = hostNodeHandleMap.toList().single { - it.second.info.advertisedServices.any { it.info.type == NetworkMapService.type } - } - - val notaryNode = hostNodeHandleMap.toList().single { - it.second.info.advertisedServices.any { it.info.type.isNotary() } + log.info("${connection.remoteNode.hostname} sees\n$pubKeysString") + hostNodeMap.put(connection.remoteNode.hostname, connection) } + val networkMapNode = hostNodeMap.values.single { it.info.advertisedServices.any { it.info.type == NetworkMapService.type } } + val notaryNode = hostNodeMap.values.single { it.info.advertisedServices.any { it.info.type.isNotary() } } val nodes = Nodes( - notary = notaryNode.second, - networkMap = networkMapNode.second, - simpleNodes = hostNodeHandleMap.values.filter { + notary = notaryNode, + networkMap = networkMapNode, + simpleNodes = hostNodeMap.values.filter { it.info.advertisedServices.none { it.info.type == NetworkMapService.type || it.info.type.isNotary() } } ) - tests.forEach { - val (test, parameters) = it + tests.forEach { (test, parameters) -> test.run(nodes, parameters, random) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt index da7590ce92..8cf9b6568b 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/LoadTestConfiguration.kt @@ -1,35 +1,45 @@ package net.corda.loadtest -import com.typesafe.config.Config -import net.corda.nodeapi.config.getValue +import net.corda.nodeapi.User import java.nio.file.Path +import java.util.concurrent.ForkJoinPool /** * @param sshUser The UNIX username to use for SSH auth. - * @param localCertificatesBaseDirectory The base directory to put node certificates in. * @param localTunnelStartingPort The local starting port to allocate tunneling ports from. * @param nodeHosts The nodes' resolvable addresses. - * @param rpcUsername The RPC user's name to establish the RPC connection as. - * @param rpcPassword The RPC user's password. + * @param rpcUser The RPC user's name and passward to establish the RPC connection. * @param remoteNodeDirectory The remote node directory. - * @param remoteMessagingPort The remote Artemis messaging port. + * @param rpcPort The remote Artemis messaging port for RPC. * @param remoteSystemdServiceName The name of the node's systemd service * @param seed An optional starting seed for the [SplittableRandom] RNG. Note that specifying the seed may not be enough * to make a load test reproducible due to unpredictable node behaviour, but it should make the local number * generation deterministic as long as [SplittableRandom.split] is used as required. This RNG is also used as input * for disruptions. + * @param mode Indicates the type of test. + * @param executionFrequency Indicates how many commands we should execute per second. + * @param generateCount Number of total commands to generate. Note that the actual number of generated commands may + * exceed this, it is used just for cutoff. + * @param parallelism Number of concurrent threads to use to run commands. Note that the actual parallelism may be + * further limited by the batches that [generate] returns. */ data class LoadTestConfiguration( - val config: Config -) { - val sshUser: String by config - val localCertificatesBaseDirectory: Path by config - val localTunnelStartingPort: Int by config - val nodeHosts: List = config.getStringList("nodeHosts") - val rpcUsername: String by config - val rpcPassword: String by config - val remoteNodeDirectory: Path by config - val remoteMessagingPort: Int by config - val remoteSystemdServiceName: String by config - val seed: Long? by config + val sshUser: String = System.getProperty("user.name"), + val localTunnelStartingPort: Int, + val nodeHosts: List, + val rpcUser: User, + val remoteNodeDirectory: Path, + val rpcPort: Int, + val remoteSystemdServiceName: String, + val seed: Long?, + val mode: TestMode = TestMode.LOAD_TEST, + val executionFrequency: Int = 20, + val generateCount: Int = 10000, + val parallelism: Int = ForkJoinPool.getCommonPoolParallelism()) + +data class RemoteNode(val hostname: String, val systemdServiceName: String, val sshUserName: String, val rpcUser: User, val rpcPort: Int, val nodeDirectory: Path) + +enum class TestMode { + LOAD_TEST, + STABILITY_TEST } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt index 8e3c39c535..0bce7c7f44 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/Main.kt @@ -2,8 +2,10 @@ package net.corda.loadtest import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions +import net.corda.loadtest.tests.StabilityTest import net.corda.loadtest.tests.crossCashTest import net.corda.loadtest.tests.selfIssueTest +import net.corda.nodeapi.config.parseAs import java.io.File /** @@ -33,6 +35,11 @@ import java.io.File * disruption is basically an infinite loop of wait->mess something up->repeat. Invariants should hold under these * conditions as well. * + * Configuration: + * The load test will look for configuration in location provided by the program argument, or the configuration can be + * provided via system properties using vm arguments, e.g. -Dloadtest.nodeHosts.0="host" see [LoadTestConfiguration] for + * list of configurable properties. + * * Diagnostic: * TODO currently the diagnostic is quite poor, all we can say is that the predicted state is different from the real * one, or that some piece of work failed to execute in some state. Logs need to be checked manually. @@ -43,26 +50,33 @@ import java.io.File */ fun main(args: Array) { - if (args.isEmpty()) { - throw IllegalArgumentException("Usage: PATH_TO_CONFIG") + val customConfig = if (args.isNotEmpty()) { + ConfigFactory.parseFile(File(args[0]), ConfigParseOptions.defaults().setAllowMissing(false)) + } else { + // This allow us to provide some configurations via teamcity. + ConfigFactory.parseProperties(System.getProperties()).getConfig("loadtest") } val defaultConfig = ConfigFactory.parseResources("loadtest-reference.conf", ConfigParseOptions.defaults().setAllowMissing(false)) - val defaultSshUserConfig = ConfigFactory.parseMap( - if (defaultConfig.hasPath("sshUser")) emptyMap() else mapOf("sshUser" to System.getProperty("user.name")) - ) - val customConfig = ConfigFactory.parseFile(File(args[0]), ConfigParseOptions.defaults().setAllowMissing(false)) - val resolvedConfig = customConfig.withFallback(defaultConfig).withFallback(defaultSshUserConfig).resolve() - val loadTestConfiguration = LoadTestConfiguration(resolvedConfig) + val resolvedConfig = customConfig.withFallback(defaultConfig).resolve() + val loadTestConfiguration = resolvedConfig.parseAs() if (loadTestConfiguration.nodeHosts.isEmpty()) { throw IllegalArgumentException("Please specify at least one node host") } + when (loadTestConfiguration.mode) { + TestMode.LOAD_TEST -> runLoadTest(loadTestConfiguration) + TestMode.STABILITY_TEST -> runStabilityTest(loadTestConfiguration) + } +} + +private fun runLoadTest(loadTestConfiguration: LoadTestConfiguration) { runLoadTests(loadTestConfiguration, listOf( selfIssueTest to LoadTest.RunParameters( parallelism = 100, generateCount = 10000, clearDatabaseBeforeRun = false, + executionFrequency = 1000, gatherFrequency = 1000, disruptionPatterns = listOf( listOf(), // no disruptions @@ -91,6 +105,7 @@ fun main(args: Array) { parallelism = 4, generateCount = 2000, clearDatabaseBeforeRun = false, + executionFrequency = 1000, gatherFrequency = 10, disruptionPatterns = listOf( listOf(), @@ -115,3 +130,26 @@ fun main(args: Array) { ) )) } + +private fun runStabilityTest(loadTestConfiguration: LoadTestConfiguration) { + runLoadTests(loadTestConfiguration, listOf( + // Self issue cash. + StabilityTest.selfIssueTest to LoadTest.RunParameters( + parallelism = loadTestConfiguration.parallelism, + generateCount = loadTestConfiguration.generateCount, + clearDatabaseBeforeRun = false, + executionFrequency = loadTestConfiguration.executionFrequency, + gatherFrequency = 100, + disruptionPatterns = listOf(listOf()) // no disruptions + ), + // Send cash to a random party or exit cash, commands are generated randomly. + StabilityTest.crossCashTest to LoadTest.RunParameters( + parallelism = loadTestConfiguration.parallelism, + generateCount = loadTestConfiguration.generateCount, + clearDatabaseBeforeRun = false, + executionFrequency = loadTestConfiguration.executionFrequency, + gatherFrequency = 100, + disruptionPatterns = listOf(listOf()) + ) + )) +} diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt new file mode 100644 index 0000000000..55cd113430 --- /dev/null +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeConnection.kt @@ -0,0 +1,169 @@ +package net.corda.loadtest + +import com.google.common.net.HostAndPort +import com.google.common.util.concurrent.ListenableFuture +import com.jcraft.jsch.ChannelExec +import com.jcraft.jsch.Session +import net.corda.client.rpc.CordaRPCClient +import net.corda.client.rpc.CordaRPCConnection +import net.corda.core.future +import net.corda.core.messaging.CordaRPCOps +import net.corda.core.node.NodeInfo +import net.corda.core.utilities.loggerFor +import net.corda.nodeapi.internal.addShutdownHook +import java.io.ByteArrayOutputStream +import java.io.Closeable +import java.io.OutputStream + +/** + * [NodeConnection] allows executing remote shell commands on the node as well as executing RPCs. + * The RPC Client start/stop must be controlled externally with [startClient] and [doWhileClientStopped]. For example + * if we want to do some action on the node that requires bringing down of the node we should nest it in a + * [doWhileClientStopped], otherwise the RPC link will be broken. + * TODO: Auto reconnect has been enable for RPC connection, investigate if we still need [doWhileClientStopped]. + */ +class NodeConnection(val remoteNode: RemoteNode, private val jSchSession: Session, private val localTunnelAddress: HostAndPort) : Closeable { + companion object { + val log = loggerFor() + } + + init { + addShutdownHook { + close() + } + } + + private val client = CordaRPCClient(localTunnelAddress) + private var rpcConnection: CordaRPCConnection? = null + val proxy: CordaRPCOps get() = rpcConnection?.proxy ?: throw IllegalStateException("proxy requested, but the client is not running") + val info: NodeInfo by lazy { proxy.nodeIdentity() } + + fun doWhileClientStopped(action: () -> A): A { + val connection = rpcConnection + require(connection != null) { "doWhileClientStopped called with no running client" } + log.info("Stopping RPC proxy to ${remoteNode.hostname}, tunnel at $localTunnelAddress") + connection!!.close() + try { + return action() + } finally { + log.info("Starting new RPC proxy to ${remoteNode.hostname}, tunnel at $localTunnelAddress") + // TODO expose these somehow? + val newConnection = client.start(remoteNode.rpcUser.username, remoteNode.rpcUser.password) + this.rpcConnection = newConnection + } + } + + fun startRPCClient() { + log.info("Creating RPC proxy to ${remoteNode.hostname}, tunnel at $localTunnelAddress") + rpcConnection = client.start(remoteNode.rpcUser.username, remoteNode.rpcUser.password) + log.info("Proxy created") + } + + /** + * @param function should call [ChannelExec.connect] + * @return A pair of (exit code, [function] return value) + */ + private fun withChannelExec(command: String, function: (ChannelExec) -> A): Pair { + val channel = jSchSession.openChannel("exec") as ChannelExec + channel.setCommand(command) + try { + val result = function(channel) + poll { channel.isEOF } + return Pair(channel.exitStatus, result) + } finally { + channel.disconnect() + } + } + + /** + * @return Pair of (stdout, stderr) of command + */ + fun runShellCommandGetOutput(command: String): ShellCommandOutput { + val stdoutStream = ByteArrayOutputStream() + val stderrStream = ByteArrayOutputStream() + val exitCode = runShellCommand(command, stdoutStream, stderrStream).get() + return ShellCommandOutput(command, exitCode, stdoutStream.toString(), stderrStream.toString()) + } + + private fun runShellCommand(command: String, stdout: OutputStream, stderr: OutputStream): ListenableFuture { + log.info("Running '$command' on ${remoteNode.hostname}") + return future { + val (exitCode, _) = withChannelExec(command) { channel -> + channel.outputStream = stdout + channel.setErrStream(stderr) + channel.connect() + poll { channel.isEOF } + } + exitCode + } + } + + data class ShellCommandOutput(val originalShellCommand: String, val exitCode: Int, val stdout: String, val stderr: String) { + fun getResultOrThrow(): String { + if (exitCode != 0) { + val diagnostic = + "There was a problem running \"$originalShellCommand\":\n" + + " stdout:\n$stdout" + + " stderr:\n$stderr" + log.error(diagnostic) + throw Exception(diagnostic) + } else { + return stdout + } + } + } + + fun startNode() { + runShellCommandGetOutput("sudo systemctl start ${remoteNode.systemdServiceName}").getResultOrThrow() + } + + fun stopNode() { + runShellCommandGetOutput("sudo systemctl stop ${remoteNode.systemdServiceName}").getResultOrThrow() + } + + fun restartNode() { + runShellCommandGetOutput("sudo systemctl restart ${remoteNode.systemdServiceName}").getResultOrThrow() + } + + fun waitUntilUp() { + log.info("Waiting for ${remoteNode.hostname} to come online") + runShellCommandGetOutput("until sudo netstat -tlpn | grep ${remoteNode.rpcPort} > /dev/null ; do sleep 1 ; done") + } + + fun getNodePid(): String { + return runShellCommandGetOutput("sudo netstat -tlpn | grep ${remoteNode.rpcPort} | awk '{print $7}' | grep -oE '[0-9]+'").getResultOrThrow().replace("\n", "") + } + + fun doWhileStopped(action: () -> A): A { + return doWhileClientStopped { + stopNode() + try { + action() + } finally { + startNode() + } + } + } + + fun kill() { + runShellCommandGetOutput("sudo kill ${getNodePid()}") + } + + fun doWhileSigStopped(action: () -> A): A { + val pid = getNodePid() + log.info("PID is $pid") + runShellCommandGetOutput("sudo kill -SIGSTOP $pid").getResultOrThrow() + try { + return action() + } finally { + runShellCommandGetOutput("sudo kill -SIGCONT $pid").getResultOrThrow() + } + } + + fun clearDb() = doWhileStopped { runShellCommandGetOutput("sudo rm ${remoteNode.nodeDirectory}/persistence.mv.db").getResultOrThrow() } + + override fun close() { + rpcConnection?.close() + jSchSession.disconnect() + } +} \ No newline at end of file diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeHandle.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeHandle.kt deleted file mode 100644 index e794dcff49..0000000000 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/NodeHandle.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.corda.loadtest - -import net.corda.core.node.NodeInfo -import org.slf4j.LoggerFactory - -private val log = LoggerFactory.getLogger(NodeHandle::class.java) - -data class NodeHandle( - val configuration: LoadTestConfiguration, - val connection: NodeConnection, - val info: NodeInfo -) - -fun NodeHandle.doWhileStopped(action: NodeHandle.() -> A): A { - return connection.doWhileClientStopped { - connection.runShellCommandGetOutput("sudo systemctl stop ${configuration.remoteSystemdServiceName}").getResultOrThrow() - try { - action() - } finally { - connection.runShellCommandGetOutput("sudo systemctl start ${configuration.remoteSystemdServiceName}").getResultOrThrow() - waitUntilUp() - } - } -} - -fun NodeHandle.doWhileSigStopped(action: NodeHandle.() -> A): A { - val pid = getNodePid() - log.info("PID is $pid") - connection.runShellCommandGetOutput("sudo kill -SIGSTOP $pid").getResultOrThrow() - try { - return action() - } finally { - connection.runShellCommandGetOutput("sudo kill -SIGCONT $pid").getResultOrThrow() - } -} - -fun NodeHandle.clearDb() = doWhileStopped { - connection.runShellCommandGetOutput("sudo rm ${configuration.remoteNodeDirectory}/persistence.mv.db").getResultOrThrow() -} - -fun NodeHandle.waitUntilUp() { - log.info("Waiting for ${info.legalIdentity} to come online") - connection.runShellCommandGetOutput("until sudo netstat -tlpn | grep ${configuration.remoteMessagingPort} > /dev/null ; do sleep 1 ; done") -} - -fun NodeHandle.getNodePid(): String { - return connection.runShellCommandGetOutput("sudo netstat -tlpn | grep ${configuration.remoteMessagingPort} | awk '{print $7}' | grep -oE '[0-9]+'").getResultOrThrow() -} 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 dba8704cd6..dbe922e5a6 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 @@ -7,13 +7,13 @@ import net.corda.contracts.asset.Cash import net.corda.core.contracts.Issued import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.USD -import net.corda.core.identity.AbstractParty import net.corda.core.failure +import net.corda.core.identity.AbstractParty import net.corda.core.serialization.OpaqueBytes import net.corda.core.success import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest -import net.corda.loadtest.NodeHandle +import net.corda.loadtest.NodeConnection import org.slf4j.LoggerFactory import java.util.* @@ -27,7 +27,7 @@ private val log = LoggerFactory.getLogger("CrossCash") data class CrossCashCommand( val command: CashFlowCommand, - val node: NodeHandle + val node: NodeConnection ) { override fun toString(): String { return when (command) { @@ -115,12 +115,12 @@ data class CrossCashState( val crossCashTest = LoadTest( "Creating Cash transactions randomly", - generate = { state, parallelism -> + generate = { (nodeVaults), parallelism -> val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } Generator.pickN(parallelism, simpleNodes).bind { nodes -> Generator.sequence( nodes.map { node -> - val quantities = state.nodeVaults[node.info.legalIdentity] ?: mapOf() + val quantities = nodeVaults[node.info.legalIdentity] ?: mapOf() val possibleRecipients = nodeMap.keys.toList() val moves = quantities.map { it.value.toDouble() / 1000 to generateMove(it.value, USD, node.info.legalIdentity, possibleRecipients) @@ -205,7 +205,7 @@ val crossCashTest = LoadTest( }, execute = { command -> - val result = command.command.startFlow(command.node.connection.proxy).returnValue + val result = command.command.startFlow(command.node.proxy).returnValue result.failure { log.error("Failure[$command]", it) } @@ -219,7 +219,7 @@ val crossCashTest = LoadTest( val currentNodeVaults = HashMap>() simpleNodes.forEach { val quantities = HashMap() - val (vault, vaultUpdates) = it.connection.proxy.vaultAndUpdates() + val (vault, vaultUpdates) = it.proxy.vaultAndUpdates() vaultUpdates.notUsed() vault.forEach { val state = it.state.data @@ -313,10 +313,10 @@ private fun searchForState( consumedTxs[originator] = 0 searchForStateHelper(state, diffIx + 1, consumedTxs, matched) var currentState = state - queue.forEachIndexed { index, pair -> + queue.forEachIndexed { index, (issuer, quantity) -> consumedTxs[originator] = index + 1 // Prune search if we exceeded the searched quantity anyway - currentState = applyDiff(pair.first, pair.second, currentState, searchedState) ?: return + currentState = applyDiff(issuer, quantity, currentState, searchedState) ?: return searchForStateHelper(currentState, diffIx + 1, consumedTxs, matched) } } 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 435809b20b..60c45a1cbf 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 @@ -13,17 +13,17 @@ import net.corda.core.success import net.corda.core.transactions.SignedTransaction import net.corda.flows.FinalityFlow import net.corda.loadtest.LoadTest -import net.corda.loadtest.NodeHandle +import net.corda.loadtest.NodeConnection import org.slf4j.LoggerFactory private val log = LoggerFactory.getLogger("NotaryTest") -data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTransaction, val node: NodeHandle) +data class NotariseCommand(val issueTx: SignedTransaction, val moveTx: SignedTransaction, val node: NodeConnection) val dummyNotarisationTest = LoadTest( "Notarising dummy transactions", generate = { _, _ -> - val generateTx = Generator.pickOne(simpleNodes).bind { node: NodeHandle -> + val generateTx = Generator.pickOne(simpleNodes).bind { node -> Generator.int().map { val issueTx = DummyContract.generateInitial(it, notary.info.notaryIdentity, DUMMY_CASH_ISSUER).apply { signWith(DUMMY_CASH_ISSUER_KEY) @@ -40,7 +40,7 @@ val dummyNotarisationTest = LoadTest( interpret = { _, _ -> }, execute = { (issueTx, moveTx, node) -> try { - val proxy = node.connection.proxy + val proxy = node.proxy val issueFlow = proxy.startFlow(::FinalityFlow, issueTx) issueFlow.returnValue.success { val moveFlow = proxy.startFlow(::FinalityFlow, moveTx) diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt index 62c4753449..6c298b981a 100644 --- a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/SelfIssueTest.kt @@ -7,12 +7,12 @@ import net.corda.client.mock.replicatePoisson import net.corda.client.rpc.notUsed import net.corda.contracts.asset.Cash import net.corda.core.contracts.USD -import net.corda.core.identity.AbstractParty import net.corda.core.flows.FlowException import net.corda.core.getOrThrow +import net.corda.core.identity.AbstractParty import net.corda.flows.CashFlowCommand import net.corda.loadtest.LoadTest -import net.corda.loadtest.NodeHandle +import net.corda.loadtest.NodeConnection import org.slf4j.LoggerFactory import java.util.* @@ -21,7 +21,7 @@ private val log = LoggerFactory.getLogger("SelfIssue") // DOCS START 1 data class SelfIssueCommand( val command: CashFlowCommand.IssueCash, - val node: NodeHandle + val node: NodeConnection ) data class SelfIssueState( @@ -37,7 +37,7 @@ val selfIssueTest = LoadTest( "Self issuing cash randomly", generate = { _, parallelism -> - val generateIssue = Generator.pickOne(simpleNodes).bind { node: NodeHandle -> + val generateIssue = Generator.pickOne(simpleNodes).bind { node -> generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { SelfIssueCommand(it, node) } @@ -61,7 +61,7 @@ val selfIssueTest = LoadTest( execute = { command -> try { - val result = command.command.startFlow(command.node.connection.proxy).returnValue.getOrThrow() + val result = command.command.startFlow(command.node.proxy).returnValue.getOrThrow() log.info("Success: $result") } catch (e: FlowException) { log.error("Failure", e) @@ -70,14 +70,14 @@ val selfIssueTest = LoadTest( gatherRemoteState = { previousState -> val selfIssueVaults = HashMap() - simpleNodes.forEach { (_, connection, info) -> + simpleNodes.forEach { connection -> val (vault, vaultUpdates) = connection.proxy.vaultAndUpdates() vaultUpdates.notUsed() vault.forEach { val state = it.state.data if (state is Cash.State) { val issuer = state.amount.token.issuer.party - if (issuer == info.legalIdentity as AbstractParty) { + if (issuer == connection.info.legalIdentity as AbstractParty) { selfIssueVaults.put(issuer, (selfIssueVaults[issuer] ?: 0L) + state.amount.quantity) } } diff --git a/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt new file mode 100644 index 0000000000..500283f630 --- /dev/null +++ b/tools/loadtest/src/main/kotlin/net/corda/loadtest/tests/StabilityTest.kt @@ -0,0 +1,70 @@ +package net.corda.loadtest.tests + +import net.corda.client.mock.Generator +import net.corda.client.mock.pickOne +import net.corda.client.mock.replicatePoisson +import net.corda.core.contracts.USD +import net.corda.core.failure +import net.corda.core.flows.FlowException +import net.corda.core.getOrThrow +import net.corda.core.success +import net.corda.core.utilities.loggerFor +import net.corda.loadtest.LoadTest + +object StabilityTest { + private val log = loggerFor() + val crossCashTest = LoadTest( + "Creating Cash transactions randomly", + generate = { _, _ -> + val nodeMap = simpleNodes.associateBy { it.info.legalIdentity } + Generator.sequence(simpleNodes.map { node -> + val possibleRecipients = nodeMap.keys.toList() + val moves = 0.5 to generateMove(1, USD, node.info.legalIdentity, possibleRecipients) + val exits = 0.5 to generateExit(1, USD) + val command = Generator.frequency(listOf(moves, exits)) + command.map { CrossCashCommand(it, nodeMap[node.info.legalIdentity]!!) } + }) + }, + interpret = { _, _ -> }, + execute = { command -> + val result = command.command.startFlow(command.node.proxy).returnValue + result.failure { + log.error("Failure[$command]", it) + } + result.success { + log.info("Success[$command]: $result") + } + }, + gatherRemoteState = {} + ) + + val selfIssueTest = LoadTest( + "Self issuing cash randomly", + generate = { _, parallelism -> + val generateIssue = Generator.pickOne(simpleNodes).bind { node -> + generateIssue(1000, USD, notary.info.notaryIdentity, listOf(node.info.legalIdentity)).map { + SelfIssueCommand(it, node) + } + } + Generator.replicatePoisson(parallelism.toDouble(), generateIssue).bind { + // We need to generate at least one + if (it.isEmpty()) { + Generator.sequence(listOf(generateIssue)) + } else { + Generator.pure(it) + } + } + }, + + interpret = { _, _ -> }, + execute = { command -> + try { + val result = command.command.startFlow(command.node.proxy).returnValue.getOrThrow() + log.info("Success: $result") + } catch (e: FlowException) { + log.error("Failure", e) + } + }, + gatherRemoteState = {} + ) +} diff --git a/tools/loadtest/src/main/resources/loadtest-reference.conf b/tools/loadtest/src/main/resources/loadtest-reference.conf index 18a7dfe80f..8b03044650 100644 --- a/tools/loadtest/src/main/resources/loadtest-reference.conf +++ b/tools/loadtest/src/main/resources/loadtest-reference.conf @@ -1,9 +1,11 @@ # nodeHosts = ["host1", "host2"] # sshUser = "someusername", by default it uses the System property "user.name" +# executionFrequency = , optional, defaulted to 20 flow execution per second. +# generateCount = , optional, defaulted to 10000. +# parallelism = , optional, defaulted to [ForkJoinPool] default parallelism. localCertificatesBaseDirectory = "build/load-test/certificates" localTunnelStartingPort = 10000 remoteNodeDirectory = "/opt/corda" -remoteMessagingPort = 10003 +rpcPort = 10003 remoteSystemdServiceName = "corda" -rpcUsername = "corda" -rpcPassword = "rgb" +rpcUser = {username = corda, password = not_blockchain, permissions = []} diff --git a/verifier/build.gradle b/verifier/build.gradle index 81747b43c3..db63013972 100644 --- a/verifier/build.gradle +++ b/verifier/build.gradle @@ -67,4 +67,13 @@ task integrationTest(type: Test) { classpath = sourceSets.integrationTest.runtimeClasspath } -build.dependsOn standaloneJar +artifacts { + publish standaloneJar { + classifier "" + } +} + +publish { + name = 'corda-verifier' + disableDefaultJar = true +} 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 96032a9df5..a343c3bd2c 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierDriver.kt @@ -15,13 +15,14 @@ import net.corda.core.random63BitValue import net.corda.core.transactions.LedgerTransaction import net.corda.core.utilities.ProcessUtilities import net.corda.core.utilities.loggerFor -import net.corda.node.driver.* import net.corda.node.services.config.configureDevKeyAndTrustStores import net.corda.nodeapi.ArtemisMessagingComponent.Companion.NODE_USER import net.corda.nodeapi.ArtemisTcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.VerifierApi +import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.SSLConfiguration +import net.corda.testing.driver.* import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.client.ActiveMQClient import org.apache.activemq.artemis.api.core.client.ClientProducer @@ -79,6 +80,7 @@ fun verifierDriver( systemProperties: Map = emptyMap(), useTestClock: Boolean = false, networkMapStartStrategy: NetworkMapStartStrategy = NetworkMapStartStrategy.Dedicated(startAutomatically = false), + startNodesInProcess: Boolean = false, dsl: VerifierExposedDSLInterface.() -> A ) = genericDriver( driverDsl = VerifierDriverDSL( @@ -89,7 +91,8 @@ fun verifierDriver( driverDirectory = driverDirectory.toAbsolutePath(), useTestClock = useTestClock, networkMapStartStrategy = networkMapStartStrategy, - isDebug = isDebug + isDebug = isDebug, + startNodesInProcess = startNodesInProcess ) ), coerce = { it }, @@ -182,8 +185,8 @@ data class VerifierDriverDSL( private fun startVerificationRequestorInternal(name: X500Name, hostAndPort: HostAndPort): VerificationRequestorHandle { val baseDir = driverDSL.driverDirectory / name.commonName - val sslConfig = object : SSLConfiguration { - override val certificatesDirectory = baseDir / "certificates" + val sslConfig = object : NodeSSLConfiguration { + override val baseDirectory = baseDir override val keyStorePassword: String get() = "cordacadevpass" override val trustStorePassword: String get() = "trustpass" } 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 7000c6b7c1..3ab800aaae 100644 --- a/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt +++ b/verifier/src/integration-test/kotlin/net/corda/verifier/VerifierTests.kt @@ -13,7 +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.testing.driver.NetworkMapStartStrategy import net.corda.node.services.config.VerifierType import net.corda.node.services.transactions.ValidatingNotaryService import org.junit.Test diff --git a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt index 333dc8a878..a42b495072 100644 --- a/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt +++ b/verifier/src/main/kotlin/net/corda/verifier/Verifier.kt @@ -5,7 +5,7 @@ import com.typesafe.config.Config import com.typesafe.config.ConfigFactory import com.typesafe.config.ConfigParseOptions import net.corda.core.ErrorOr -import net.corda.core.internal.addShutdownHook +import net.corda.nodeapi.internal.addShutdownHook import net.corda.core.div import net.corda.core.utilities.debug import net.corda.core.utilities.loggerFor @@ -13,20 +13,19 @@ import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.VerifierApi import net.corda.nodeapi.VerifierApi.VERIFICATION_REQUESTS_QUEUE_NAME -import net.corda.nodeapi.config.SSLConfiguration +import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue import org.apache.activemq.artemis.api.core.client.ActiveMQClient import java.nio.file.Path import java.nio.file.Paths data class VerifierConfiguration( - val baseDirectory: Path, + override val baseDirectory: Path, val config: Config -) : SSLConfiguration { +) : NodeSSLConfiguration { val nodeHostAndPort: HostAndPort by config override val keyStorePassword: String by config override val trustStorePassword: String by config - override val certificatesDirectory = baseDirectory / "certificates" } class Verifier { 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 5429c535eb..b8f4bccf04 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 @@ -11,6 +11,7 @@ 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.identity.PartyAndCertificate import net.corda.core.node.ServicesForResolution import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentsStorageService @@ -40,7 +41,7 @@ private class ServicesForVerification(dependenciesList: List, a get() = throw UnsupportedOperationException() set(value) = throw UnsupportedOperationException() - override fun getAllIdentities() = emptyList() + override fun getAllIdentities() = emptyList() private val dependencies = dependenciesList.associateBy { it.id } override val identityService: IdentityService = this @@ -53,10 +54,11 @@ 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 registerIdentity(party: PartyAndCertificate) = TODO("not implemented") + override fun registerAnonymousIdentity(anonymousParty: AnonymousParty, party: Party, path: CertPath) = TODO("not implemented") + override fun certificateFromParty(party: Party): PartyAndCertificate? = TODO("not implemented") + override fun requirePartyFromAnonymous(party: AbstractParty): Party = TODO("not implemented") + override fun partiesFromName(query: String, exactMatch: Boolean): Set = 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 diff --git a/webserver/build.gradle b/webserver/build.gradle index 0cb4d9452b..dc2e70e676 100644 --- a/webserver/build.gradle +++ b/webserver/build.gradle @@ -33,7 +33,6 @@ dependencies { compile project(':finance') compile project(':client:rpc') compile project(':client:jackson') - testCompile project(':node') // Web stuff: for HTTP[S] servlets compile "org.eclipse.jetty:jetty-servlet:$jetty_version" @@ -58,6 +57,7 @@ dependencies { // For rendering the index page. compile "org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.3" + testCompile project(':test-utils') testCompile "junit:junit:$junit_version" } @@ -65,3 +65,11 @@ task integrationTest(type: Test) { testClassesDir = sourceSets.integrationTest.output.classesDir classpath = sourceSets.integrationTest.runtimeClasspath } + +jar { + baseName 'corda-webserver-impl' +} + +publish { + name = jar.baseName +} diff --git a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt index 7dad7ecae1..dbbab3e2df 100644 --- a/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt +++ b/webserver/src/integration-test/kotlin/net/corda/webserver/WebserverDriverTests.kt @@ -3,16 +3,17 @@ package net.corda.webserver import com.google.common.net.HostAndPort import net.corda.core.getOrThrow import net.corda.core.utilities.DUMMY_BANK_A -import net.corda.node.driver.WebserverHandle -import net.corda.node.driver.addressMustBeBound -import net.corda.node.driver.addressMustNotBeBound -import net.corda.node.driver.driver +import net.corda.testing.driver.WebserverHandle +import net.corda.testing.driver.addressMustBeBound +import net.corda.testing.driver.addressMustNotBeBound +import net.corda.testing.driver.driver import org.junit.Test import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService class DriverTests { companion object { - val executorService = Executors.newScheduledThreadPool(2) + val executorService: ScheduledExecutorService = Executors.newScheduledThreadPool(2) fun webserverMustBeUp(webserverHandle: WebserverHandle) { addressMustBeBound(executorService, webserverHandle.listenAddress, webserverHandle.process) diff --git a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt index ad17a38ed1..194536869e 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/WebServerConfig.kt @@ -2,16 +2,14 @@ package net.corda.webserver import com.google.common.net.HostAndPort import com.typesafe.config.Config -import net.corda.core.div -import net.corda.nodeapi.config.SSLConfiguration +import net.corda.nodeapi.config.NodeSSLConfiguration import net.corda.nodeapi.config.getValue import java.nio.file.Path /** * [baseDirectory] is not retrieved from the config file but rather from a command line argument. */ -class WebServerConfig(val baseDirectory: Path, val config: Config) : SSLConfiguration { - override val certificatesDirectory: Path get() = baseDirectory / "certificates" +class WebServerConfig(override val baseDirectory: Path, val config: Config) : NodeSSLConfiguration { override val keyStorePassword: String by config override val trustStorePassword: String by config val exportJMXto: String get() = "http" @@ -19,4 +17,4 @@ class WebServerConfig(val baseDirectory: Path, val config: Config) : SSLConfigur val myLegalName: String by config val p2pAddress: HostAndPort by config // TODO: Use RPC port instead of P2P port (RPC requires authentication, P2P does not) val webAddress: HostAndPort by config -} \ No newline at end of file +} diff --git a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt index 958465bdc5..6ad3c21ec9 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/internal/NodeWebServer.kt @@ -3,10 +3,10 @@ package net.corda.webserver.internal import com.google.common.html.HtmlEscapers.htmlEscaper import net.corda.client.rpc.CordaRPCClient import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.CordaPluginRegistry import net.corda.core.utilities.loggerFor import net.corda.nodeapi.ArtemisMessagingComponent import net.corda.webserver.WebServerConfig +import net.corda.webserver.services.WebServerPluginRegistry import net.corda.webserver.servlets.* import org.apache.activemq.artemis.api.core.ActiveMQNotConnectedException import org.eclipse.jetty.server.* @@ -203,9 +203,9 @@ class NodeWebServer(val config: WebServerConfig) { return connection.proxy } - /** Fetch CordaPluginRegistry classes registered in META-INF/services/net.corda.core.node.CordaPluginRegistry files that exist in the classpath */ - val pluginRegistries: List by lazy { - ServiceLoader.load(CordaPluginRegistry::class.java).toList() + /** Fetch WebServerPluginRegistry classes registered in META-INF/services/net.corda.webserver.services.WebServerPluginRegistry files that exist in the classpath */ + val pluginRegistries: List by lazy { + ServiceLoader.load(WebServerPluginRegistry::class.java).toList() } /** Used for useful info that we always want to show, even when not logging to the console */ diff --git a/webserver/src/main/kotlin/net/corda/webserver/service/WebServerPluginRegistry.kt b/webserver/src/main/kotlin/net/corda/webserver/service/WebServerPluginRegistry.kt new file mode 100644 index 0000000000..69f5cb612c --- /dev/null +++ b/webserver/src/main/kotlin/net/corda/webserver/service/WebServerPluginRegistry.kt @@ -0,0 +1,24 @@ +package net.corda.webserver.services + +import net.corda.core.messaging.CordaRPCOps +import java.util.function.Function + +/** + * Implement this interface on a class advertised in a META-INF/services/net.corda.webserver.services.WebServerPluginRegistry file + * to create web API to connect to Corda node via RPC. + */ +interface WebServerPluginRegistry { + /** + * List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver lives + * in a process separate from the node itself. + */ + 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() + */ + val staticServeDirs: Map get() = emptyMap() + +} \ No newline at end of file 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 8de361d8a1..eba6597428 100644 --- a/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt +++ b/webserver/src/main/kotlin/net/corda/webserver/servlets/CorDappInfoServlet.kt @@ -3,7 +3,7 @@ package net.corda.webserver.servlets import kotlinx.html.* import kotlinx.html.stream.appendHTML import net.corda.core.messaging.CordaRPCOps -import net.corda.core.node.CordaPluginRegistry +import net.corda.webserver.services.WebServerPluginRegistry import org.glassfish.jersey.server.model.Resource import org.glassfish.jersey.server.model.ResourceMethod import java.io.IOException @@ -15,7 +15,7 @@ import javax.servlet.http.HttpServletResponse * Dumps some data about the installed CorDapps. * TODO: Add registered flow initiators. */ -class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps): HttpServlet() { +class CorDappInfoServlet(val plugins: List, val rpc: CordaRPCOps): HttpServlet() { @Throws(IOException::class) override fun doGet(req: HttpServletRequest, resp: HttpServletResponse) { diff --git a/webserver/webcapsule/build.gradle b/webserver/webcapsule/build.gradle index 55844b5ff6..08a8f0a9e1 100644 --- a/webserver/webcapsule/build.gradle +++ b/webserver/webcapsule/build.gradle @@ -23,7 +23,7 @@ task buildWebserverJar(type: FatCapsule, dependsOn: project(':node').compileJava applicationClass 'net.corda.webserver.WebServer' archiveName "corda-webserver-${corda_release_version}.jar" applicationSource = files( - project(':webserver').configurations.compile, + project(':webserver').configurations.runtime, project(':webserver').jar, new File(project(':node').buildDir, 'classes/main/CordaCaplet.class'), new File(project(':node').buildDir, 'classes/main/CordaCaplet$1.class'),