Merge remote-tracking branch 'open/master'

This commit is contained in:
Andrius Dagys 2017-05-24 11:16:26 +01:00
commit 376a9d399f
314 changed files with 5179 additions and 3073 deletions

3
.gitignore vendored
View File

@ -32,6 +32,7 @@ lib/dokka.jar
.idea/libraries .idea/libraries
.idea/shelf .idea/shelf
.idea/dataSources .idea/dataSources
/gradle-plugins/.idea
# Include the -parameters compiler option by default in IntelliJ required for serialization. # Include the -parameters compiler option by default in IntelliJ required for serialization.
!.idea/compiler.xml !.idea/compiler.xml
@ -53,6 +54,7 @@ lib/dokka.jar
# Gradle: # Gradle:
# .idea/gradle.xml # .idea/gradle.xml
# .idea/libraries # .idea/libraries
/gradle-plugins/gradle*
# Mongo Explorer plugin: # Mongo Explorer plugin:
# .idea/mongoSettings.xml # .idea/mongoSettings.xml
@ -65,6 +67,7 @@ lib/dokka.jar
# IntelliJ # IntelliJ
/out/ /out/
/classes/
# mpeltonen/sbt-idea plugin # mpeltonen/sbt-idea plugin
.idea_modules/ .idea_modules/

5
.idea/compiler.xml generated
View File

@ -17,6 +17,8 @@
<module name="corda-webserver_integrationTest" target="1.8" /> <module name="corda-webserver_integrationTest" target="1.8" />
<module name="corda-webserver_main" target="1.8" /> <module name="corda-webserver_main" target="1.8" />
<module name="corda-webserver_test" target="1.8" /> <module name="corda-webserver_test" target="1.8" />
<module name="cordform-common_main" target="1.8" />
<module name="cordform-common_test" target="1.8" />
<module name="core_main" target="1.8" /> <module name="core_main" target="1.8" />
<module name="core_test" target="1.8" /> <module name="core_test" target="1.8" />
<module name="demobench_main" target="1.8" /> <module name="demobench_main" target="1.8" />
@ -59,10 +61,13 @@
<module name="node_integrationTest" target="1.8" /> <module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" /> <module name="node_main" target="1.8" />
<module name="node_test" target="1.8" /> <module name="node_test" target="1.8" />
<module name="quasar-hook_main" target="1.8" />
<module name="quasar-hook_test" target="1.8" />
<module name="raft-notary-demo_main" target="1.8" /> <module name="raft-notary-demo_main" target="1.8" />
<module name="raft-notary-demo_test" target="1.8" /> <module name="raft-notary-demo_test" target="1.8" />
<module name="rpc_integrationTest" target="1.8" /> <module name="rpc_integrationTest" target="1.8" />
<module name="rpc_main" target="1.8" /> <module name="rpc_main" target="1.8" />
<module name="rpc_smokeTest" target="1.8" />
<module name="rpc_test" target="1.8" /> <module name="rpc_test" target="1.8" />
<module name="samples_main" target="1.8" /> <module name="samples_main" target="1.8" />
<module name="samples_test" target="1.8" /> <module name="samples_test" target="1.8" />

View File

@ -16,6 +16,11 @@ buildscript {
// TODO: Sort this alphabetically. // TODO: Sort this alphabetically.
ext.kotlin_version = constants.getProperty("kotlinVersion") ext.kotlin_version = constants.getProperty("kotlinVersion")
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved. ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
ext.capsule_version = '1.0.1'
ext.asm_version = '0.5.3' ext.asm_version = '0.5.3'
ext.artemis_version = '1.5.3' ext.artemis_version = '1.5.3'
ext.jackson_version = '2.8.5' ext.jackson_version = '2.8.5'
@ -41,6 +46,7 @@ buildscript {
ext.rxjava_version = '1.2.4' ext.rxjava_version = '1.2.4'
ext.requery_version = '1.2.1' ext.requery_version = '1.2.1'
ext.dokka_version = '0.9.13' ext.dokka_version = '0.9.13'
ext.eddsa_version = '0.2.0'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest: // Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
ext.java8_minUpdateVersion = '131' ext.java8_minUpdateVersion = '131'
@ -60,12 +66,14 @@ buildscript {
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}" classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
classpath "org.ajoberstar:grgit:1.1.0" classpath "org.ajoberstar:grgit:1.1.0"
classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment.
} }
} }
plugins { plugins {
// TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style, // TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style,
// but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle.
// Version 1.0.2 of this plugin uses capsule:1.0.1
id "us.kirchmeier.capsule" version "1.0.2" id "us.kirchmeier.capsule" version "1.0.2"
} }
@ -249,7 +257,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda' projectUrl = 'https://github.com/corda/corda'
gpgSign = true gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver'] publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
license { license {
name = 'Apache-2.0' name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0'

View File

@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.BusinessCalendar import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
@ -43,18 +44,21 @@ object JacksonSupport {
} }
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName) override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal) override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey) override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
} }
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName) override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal) override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey) override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
} }
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) { class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException() override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException() override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException() override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
@ -66,6 +70,7 @@ object JacksonSupport {
addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer) addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer)
addSerializer(Party::class.java, PartySerializer) addSerializer(Party::class.java, PartySerializer)
addDeserializer(Party::class.java, PartyDeserializer) addDeserializer(Party::class.java, PartyDeserializer)
addDeserializer(AbstractParty::class.java, PartyDeserializer)
addSerializer(BigDecimal::class.java, ToStringSerializer) addSerializer(BigDecimal::class.java, ToStringSerializer)
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer()) addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
addSerializer(SecureHash::class.java, SecureHashSerializer) addSerializer(SecureHash::class.java, SecureHashSerializer)
@ -160,8 +165,20 @@ object JacksonSupport {
} }
val mapper = parser.codec as PartyObjectMapper val mapper = parser.codec as PartyObjectMapper
// TODO: We should probably have a better specified way of identifying X.500 names vs keys
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
// how to parse the content
return if (parser.text.contains("=")) {
val principal = X500Name(parser.text) val principal = X500Name(parser.text)
return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}") mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
} else {
val key = try {
parsePublicKeyBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key")
}
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
}
} }
} }

View File

@ -193,8 +193,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
val parameterString = "{ $args }" val parameterString = "{ $args }"
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args) val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args) if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
val inOrderParams: List<Any?> = parameters.mapIndexed { _, param -> val inOrderParams: List<Any?> = parameters.mapIndexed { _, (argName, argType) ->
val (argName, argType) = param
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args) val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
try { try {
om.readValue(entry.traverse(om), argType) om.readValue(entry.traverse(om), argType)

View File

@ -1,14 +1,16 @@
package net.corda.jackson package net.corda.jackson
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import org.junit.Assert.assertArrayEquals
import org.junit.Test import org.junit.Test
import kotlin.reflect.full.primaryConstructor
import kotlin.test.assertEquals import kotlin.test.assertEquals
class StringToMethodCallParserTest { class StringToMethodCallParserTest {
@Suppress("UNUSED") @Suppress("UNUSED")
class Target { class Target {
fun simple() = "simple" fun simple() = "simple"
fun string(note: String) = note fun string(noteTextWord: String) = noteTextWord
fun twoStrings(a: String, b: String) = a + b fun twoStrings(a: String, b: String) = a + b
fun simpleObject(hash: SecureHash.SHA256) = hash.toString() fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
fun complexObject(pair: Pair<Int, String>) = pair fun complexObject(pair: Pair<Int, String>) = pair
@ -20,7 +22,7 @@ class StringToMethodCallParserTest {
val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401" val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401"
val tests = mapOf( val tests = mapOf(
"simple" to "simple", "simple" to "simple",
"string note: A test of barewords" to "A test of barewords", "string noteTextWord: A test of barewords" to "A test of barewords",
"twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak", "twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak",
"simpleObject hash: $randomHash" to randomHash.toUpperCase(), "simpleObject hash: $randomHash" to randomHash.toUpperCase(),
"complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother"), "complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother"),
@ -36,4 +38,31 @@ class StringToMethodCallParserTest {
assertEquals(output, parser.parse(target, input).invoke()) assertEquals(output, parser.parse(target, input).invoke())
} }
} }
@Suppress("UNUSED")
class ConstructorTarget(val someWord: String, val aDifferentThing: Int) {
constructor(alternativeWord: String) : this(alternativeWord, 0)
}
@Test
fun ctor1() {
val clazz = ConstructorTarget::class.java
val parser = StringToMethodCallParser(clazz)
val ctor = clazz.constructors.single { it.parameterCount == 2 }
val names: List<String> = parser.paramNamesFromConstructor(ctor)
assertEquals(listOf("someWord", "aDifferentThing"), names)
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "someWord: Blah blah blah, aDifferentThing: 12")
assertArrayEquals(args, arrayOf<Any?>("Blah blah blah", 12))
}
@Test
fun ctor2() {
val clazz = ConstructorTarget::class.java
val parser = StringToMethodCallParser(clazz)
val ctor = clazz.constructors.single { it.parameterCount == 1 }
val names: List<String> = parser.paramNamesFromConstructor(ctor)
assertEquals(listOf("alternativeWord"), names)
val args: Array<Any?> = parser.parseArguments(clazz.name, names.zip(ctor.parameterTypes), "alternativeWord: Foo bar!")
assertArrayEquals(args, arrayOf<Any?>("Foo bar!"))
}
} }

View File

@ -11,6 +11,9 @@ configurations {
integrationTestCompile.extendsFrom testCompile integrationTestCompile.extendsFrom testCompile
integrationTestRuntime.extendsFrom testRuntime integrationTestRuntime.extendsFrom testRuntime
smokeTestCompile.extendsFrom compile
smokeTestRuntime.extendsFrom runtime
} }
sourceSets { sourceSets {
@ -21,6 +24,24 @@ sourceSets {
srcDir file('src/integration-test/kotlin') srcDir file('src/integration-test/kotlin')
} }
} }
smokeTest {
kotlin {
// We must NOT have any Node code on the classpath, so do NOT
// include the test or integrationTest dependencies here.
compileClasspath += main.output
runtimeClasspath += main.output
srcDir file('src/smoke-test/kotlin')
}
}
}
processSmokeTestResources {
from(file("$rootDir/config/test/log4j2.xml")) {
rename 'log4j2\\.xml', 'log4j2-test.xml'
}
from(project(':node:capsule').tasks.buildCordaJAR) {
rename 'corda-(.*)', 'corda.jar'
}
} }
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in // To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
@ -38,11 +59,22 @@ dependencies {
testCompile project(':test-utils') testCompile project(':test-utils')
testCompile project(':client:mock') testCompile project(':client:mock')
// Integration test helpers // Smoke tests do NOT have any Node code on the classpath!
integrationTestCompile "junit:junit:$junit_version" smokeTestCompile project(':finance')
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
smokeTestCompile "org.assertj:assertj-core:${assertj_version}"
smokeTestCompile "junit:junit:$junit_version"
} }
task integrationTest(type: Test) { task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath classpath = sourceSets.integrationTest.runtimeClasspath
} }
task smokeTest(type: Test) {
testClassesDir = sourceSets.smokeTest.output.classesDir
classpath = sourceSets.smokeTest.runtimeClasspath
systemProperties['build.dir'] = buildDir
}

View File

@ -5,26 +5,181 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.*
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.millis import net.corda.node.driver.poll
import net.corda.core.random63BitValue
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCKryo import net.corda.nodeapi.RPCKryo
import net.corda.testing.* import net.corda.testing.*
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.time.Duration 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.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class RPCStabilityTests { class RPCStabilityTests {
object DummyOps : RPCOps {
override val protocolVersion = 0
}
private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Int {
val values = ConcurrentLinkedQueue<Int>()
return poll(executorService, "number of threads to become stable", 250.millis) {
values.add(Thread.activeCount())
if (values.size > 5) {
values.poll()
}
val first = values.peek()
if (values.size == 5 && values.all { it == first }) {
first
} else {
null
}
}.get()
}
@Test
fun `client and server dont leak threads`() {
val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() {
rpcDriver {
val server = startRpcServer<RPCOps>(ops = DummyOps)
startRpcClient<RPCOps>(server.get().broker.hostAndPort!!).get()
}
}
repeat(5) {
startAndStop()
}
val numberOfThreadsBefore = waitUntilNumberOfThreadsStable(executor)
repeat(5) {
startAndStop()
}
val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor)
// This is a less than check because threads from other tests may be shutting down while this test is running.
// This is therefore a "best effort" check. When this test is run on its own this should be a strict equality.
assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter)
executor.shutdownNow()
}
@Test
fun `client doesnt leak threads when it fails to start`() {
val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() {
rpcDriver {
ErrorOr.catch { startRpcClient<RPCOps>(HostAndPort.fromString("localhost:9999")).get() }
val server = startRpcServer<RPCOps>(ops = DummyOps)
ErrorOr.catch { startRpcClient<RPCOps>(
server.get().broker.hostAndPort!!,
configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1)
).get() }
}
}
repeat(5) {
startAndStop()
}
val numberOfThreadsBefore = waitUntilNumberOfThreadsStable(executor)
repeat(5) {
startAndStop()
}
val numberOfThreadsAfter = waitUntilNumberOfThreadsStable(executor)
assertTrue(numberOfThreadsBefore >= numberOfThreadsAfter)
executor.shutdownNow()
}
fun RpcBrokerHandle.getStats(): Map<String, Any> {
return serverControl.run {
mapOf(
"connections" to listConnectionIDs().toSet(),
"sessionCount" to listConnectionIDs().flatMap { listSessions(it).toList() }.size,
"consumerCount" to totalConsumerCount
)
}
}
@Test
fun `rpc server close doesnt leak broker resources`() {
rpcDriver {
fun startAndCloseServer(broker: RpcBrokerHandle) {
startRpcServerWithBrokerRunning(
configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1),
ops = DummyOps,
brokerHandle = broker
).rpcServer.close()
}
val broker = startRpcBroker().get()
startAndCloseServer(broker)
val initial = broker.getStats()
repeat(100) {
startAndCloseServer(broker)
}
pollUntilTrue("broker resources to be released") {
initial == broker.getStats()
}
}
}
@Test
fun `rpc client close doesnt leak broker resources`() {
rpcDriver {
val server = startRpcServer(configuration = RPCServerConfiguration.default.copy(consumerPoolSize = 1, producerPoolBound = 1), ops = DummyOps).get()
RPCClient<RPCOps>(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password).close()
val initial = server.broker.getStats()
repeat(100) {
val connection = RPCClient<RPCOps>(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password)
connection.close()
}
pollUntilTrue("broker resources to be released") {
initial == server.broker.getStats()
}
}
}
@Test
fun `rpc server close is idempotent`() {
rpcDriver {
val server = startRpcServer(ops = DummyOps).get()
repeat(10) {
server.rpcServer.close()
}
}
}
@Test
fun `rpc client close is idempotent`() {
rpcDriver {
val serverShutdown = shutdownManager.follower()
val server = startRpcServer(ops = DummyOps).get()
serverShutdown.unfollow()
// With the server up
val connection1 = RPCClient<RPCOps>(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password)
repeat(10) {
connection1.close()
}
val connection2 = RPCClient<RPCOps>(server.broker.hostAndPort!!).start(RPCOps::class.java, rpcTestUser.username, rpcTestUser.password)
serverShutdown.shutdown()
// With the server down
repeat(10) {
connection2.close()
}
}
}
interface LeakObservableOps: RPCOps { interface LeakObservableOps: RPCOps {
fun leakObservable(): Observable<Nothing> fun leakObservable(): Observable<Nothing>
} }
@ -42,7 +197,7 @@ class RPCStabilityTests {
} }
} }
val server = startRpcServer<LeakObservableOps>(ops = leakObservableOpsImpl) val server = startRpcServer<LeakObservableOps>(ops = leakObservableOpsImpl)
val proxy = startRpcClient<LeakObservableOps>(server.get().hostAndPort).get() val proxy = startRpcClient<LeakObservableOps>(server.get().broker.hostAndPort!!).get()
// Leak many observables // Leak many observables
val N = 200 val N = 200
(1..N).toList().parallelStream().forEach { (1..N).toList().parallelStream().forEach {
@ -57,6 +212,31 @@ class RPCStabilityTests {
} }
} }
interface ReconnectOps : RPCOps {
fun ping(): String
}
@Test
fun `client reconnects to rebooted server`() {
rpcDriver {
val ops = object : ReconnectOps {
override val protocolVersion = 0
override fun ping() = "pong"
}
val serverFollower = shutdownManager.follower()
val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!!
serverFollower.unfollow()
val clientFollower = shutdownManager.follower()
val client = startRpcClient<ReconnectOps>(serverPort).getOrThrow()
clientFollower.unfollow()
assertEquals("pong", client.ping())
serverFollower.shutdown()
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
assertEquals("pong", client.ping())
clientFollower.shutdown() // Driver would do this after the new server, causing hang.
}
}
interface TrackSubscriberOps : RPCOps { interface TrackSubscriberOps : RPCOps {
fun subscribe(): Observable<Unit> fun subscribe(): Observable<Unit>
} }
@ -86,7 +266,7 @@ class RPCStabilityTests {
val numberOfClients = 4 val numberOfClients = 4
val clients = Futures.allAsList((1 .. numberOfClients).map { val clients = Futures.allAsList((1 .. numberOfClients).map {
startRandomRpcClient<TrackSubscriberOps>(server.hostAndPort) startRandomRpcClient<TrackSubscriberOps>(server.broker.hostAndPort!!)
}).get() }).get()
// Poll until all clients connect // Poll until all clients connect
@ -131,7 +311,7 @@ class RPCStabilityTests {
// Construct an RPC session manually so that we can hang in the message handler // Construct an RPC session manually so that we can hang in the message handler
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}" val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
val session = startArtemisSession(server.hostAndPort) val session = startArtemisSession(server.broker.hostAndPort!!)
session.createTemporaryQueue(myQueue, myQueue) session.createTemporaryQueue(myQueue, myQueue)
val consumer = session.createConsumer(myQueue, null, -1, -1, false) val consumer = session.createConsumer(myQueue, null, -1, -1, false)
consumer.setMessageHandler { consumer.setMessageHandler {
@ -163,7 +343,7 @@ class RPCStabilityTests {
fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) { fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
pollUntilTrue("number of RPC clients to become $expected") { pollUntilTrue("number of RPC clients to become $expected") {
val clientAddresses = server.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) } val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
clientAddresses.size == expected clientAddresses.size == expected
}.get() }.get()
} }

View File

@ -9,10 +9,12 @@ import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.SSLConfiguration
import java.time.Duration import java.time.Duration
/** @see RPCClient.RPCConnection */
class CordaRPCConnection internal constructor( class CordaRPCConnection internal constructor(
connection: RPCClient.RPCConnection<CordaRPCOps> connection: RPCClient.RPCConnection<CordaRPCOps>
) : RPCClient.RPCConnection<CordaRPCOps> by connection ) : RPCClient.RPCConnection<CordaRPCOps> by connection
/** @see RPCClientConfiguration */
data class CordaRPCClientConfiguration( data class CordaRPCClientConfiguration(
val connectionMaxRetryInterval: Duration val connectionMaxRetryInterval: Duration
) { ) {
@ -29,6 +31,7 @@ data class CordaRPCClientConfiguration(
} }
} }
/** @see RPCClient */
class CordaRPCClient( class CordaRPCClient(
hostAndPort: HostAndPort, hostAndPort: HostAndPort,
sslConfiguration: SSLConfiguration? = null, sslConfiguration: SSLConfiguration? = null,

View File

@ -53,10 +53,12 @@ data class RPCClientConfiguration(
val connectionRetryIntervalMultiplier: Double, val connectionRetryIntervalMultiplier: Double,
/** Maximum retry interval */ /** Maximum retry interval */
val connectionMaxRetryInterval: Duration, val connectionMaxRetryInterval: Duration,
val maxReconnectAttempts: Int,
/** Maximum file size */ /** Maximum file size */
val maxFileSize: Int val maxFileSize: Int
) { ) {
companion object { companion object {
val unlimitedReconnectAttempts = -1
@JvmStatic @JvmStatic
val default = RPCClientConfiguration( val default = RPCClientConfiguration(
minimumServerProtocolVersion = 0, minimumServerProtocolVersion = 0,
@ -68,6 +70,7 @@ data class RPCClientConfiguration(
connectionRetryInterval = 5.seconds, connectionRetryInterval = 5.seconds,
connectionRetryIntervalMultiplier = 1.5, connectionRetryIntervalMultiplier = 1.5,
connectionMaxRetryInterval = 3.minutes, connectionMaxRetryInterval = 3.minutes,
maxReconnectAttempts = unlimitedReconnectAttempts,
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */ /** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
maxFileSize = 10485760 maxFileSize = 10485760
) )
@ -114,9 +117,9 @@ class RPCClient<I : RPCOps>(
* *
* The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object * The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object
* graph returned then the server-side observable is transparently forwarded to the client side here. * graph returned then the server-side observable is transparently forwarded to the client side here.
* *You are expected to use it*. The server will begin buffering messages immediately that it will expect you to * *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the
* drain by subscribing to the returned observer. You can opt-out of this by simply calling the * client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply
* [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually * calling the [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually
* subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself * subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself
* is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are * is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are
* unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't * unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't
@ -139,10 +142,12 @@ class RPCClient<I : RPCOps>(
retryInterval = rpcConfiguration.connectionRetryInterval.toMillis() retryInterval = rpcConfiguration.connectionRetryInterval.toMillis()
retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier
maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis() maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis()
reconnectAttempts = rpcConfiguration.maxReconnectAttempts
minLargeMessageSize = rpcConfiguration.maxFileSize minLargeMessageSize = rpcConfiguration.maxFileSize
} }
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass) val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass)
try {
proxyHandler.start() proxyHandler.start()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -164,6 +169,11 @@ class RPCClient<I : RPCOps>(
serverLocator.close() serverLocator.close()
} }
} }
} catch (exception: Throwable) {
proxyHandler.close()
serverLocator.close()
throw exception
}
} }
} }
} }

View File

@ -25,16 +25,11 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator
import rx.Notification import rx.Notification
import rx.Observable import rx.Observable
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import sun.reflect.CallerSensitive
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.Method import java.lang.reflect.Method
import java.util.* import java.util.*
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.*
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.ArrayList
import kotlin.reflect.jvm.javaMethod import kotlin.reflect.jvm.javaMethod
/** /**
@ -81,16 +76,13 @@ class RPCClientProxyHandler(
val log = loggerFor<RPCClientProxyHandler>() val log = loggerFor<RPCClientProxyHandler>()
// Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context // Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context
// to do that. However it may still be used for serialisation of RPC requests and related messages. // to do that. However it may still be used for serialisation of RPC requests and related messages.
val kryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build() val kryoPool: KryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
// To check whether toString() is being invoked // To check whether toString() is being invoked
val toStringMethod: Method = Object::toString.javaMethod!! val toStringMethod: Method = Object::toString.javaMethod!!
} }
// Used for reaping // Used for reaping
private val reaperExecutor = Executors.newScheduledThreadPool( private var reaperExecutor: ScheduledExecutorService? = null
1,
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
)
// A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering. // A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build() private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build()
@ -109,7 +101,7 @@ class RPCClientProxyHandler(
hardReferenceStore = Collections.synchronizedSet(mutableSetOf<Observable<*>>()) hardReferenceStore = Collections.synchronizedSet(mutableSetOf<Observable<*>>())
) )
// Holds a reference to the scheduled reaper. // Holds a reference to the scheduled reaper.
private lateinit var reaperScheduledFuture: ScheduledFuture<*> private var reaperScheduledFuture: ScheduledFuture<*>? = null
// The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion] // The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion]
private var serverProtocolVersion: Int? = null private var serverProtocolVersion: Int? = null
@ -145,7 +137,7 @@ class RPCClientProxyHandler(
// TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a // TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a
// single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is // single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is
// integrated properly. // integrated properly.
private lateinit var sessionAndConsumer: ArtemisConsumer private var sessionAndConsumer: ArtemisConsumer? = null
// Pool producers to reduce contention on the client side. // Pool producers to reduce contention on the client side.
private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) { private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) {
// Note how we create new sessions *and* session factories per producer. // Note how we create new sessions *and* session factories per producer.
@ -162,7 +154,12 @@ class RPCClientProxyHandler(
* Start the client. This creates the per-client queue, starts the consumer session and the reaper. * Start the client. This creates the per-client queue, starts the consumer session and the reaper.
*/ */
fun start() { fun start() {
reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate( lifeCycle.requireState(State.UNSTARTED)
reaperExecutor = Executors.newScheduledThreadPool(
1,
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
)
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
this::reapObservables, this::reapObservables,
rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(),
rpcConfiguration.reapInterval.toMillis(), rpcConfiguration.reapInterval.toMillis(),
@ -187,7 +184,7 @@ class RPCClientProxyHandler(
if (method == toStringMethod) { if (method == toStringMethod) {
return "Client RPC proxy for $rpcOpsClass" return "Client RPC proxy for $rpcOpsClass"
} }
if (sessionAndConsumer.session.isClosed) { if (sessionAndConsumer!!.session.isClosed) {
throw RPCException("RPC Proxy is closed") throw RPCException("RPC Proxy is closed")
} }
val rpcId = RPCApi.RpcRequestId(random63BitValue()) val rpcId = RPCApi.RpcRequestId(random63BitValue())
@ -211,6 +208,12 @@ class RPCClientProxyHandler(
it.session.commit() it.session.commit()
} }
return replyFuture.getOrThrow() return replyFuture.getOrThrow()
} catch (e: RuntimeException) {
// Already an unchecked exception, so just rethrow it
throw e
} catch (e: Exception) {
// This must be a checked exception, so wrap it
throw RPCException(e.message ?: "", e)
} finally { } finally {
callSiteMap?.remove(rpcId.toLong) callSiteMap?.remove(rpcId.toLong)
} }
@ -268,24 +271,19 @@ class RPCClientProxyHandler(
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors. * Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
*/ */
fun close() { fun close() {
sessionAndConsumer.consumer.close() sessionAndConsumer?.sessionFactory?.close()
sessionAndConsumer.session.close() reaperScheduledFuture?.cancel(false)
sessionAndConsumer.sessionFactory.close()
reaperScheduledFuture.cancel(false)
observableContext.observableMap.invalidateAll() observableContext.observableMap.invalidateAll()
reapObservables() reapObservables()
reaperExecutor.shutdownNow() reaperExecutor?.shutdownNow()
sessionAndProducerPool.close().forEach { sessionAndProducerPool.close().forEach {
it.producer.close()
it.session.close()
it.sessionFactory.close() it.sessionFactory.close()
} }
// Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may // Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may
// leak borrowed executors. // leak borrowed executors.
val observationExecutors = observationExecutorPool.close() val observationExecutors = observationExecutorPool.close()
observationExecutors.forEach { it.shutdownNow() } observationExecutors.forEach { it.shutdownNow() }
observationExecutors.forEach { it.awaitTermination(100, TimeUnit.MILLISECONDS) } lifeCycle.justTransition(State.FINISHED)
lifeCycle.transition(State.STARTED, State.FINISHED)
} }
/** /**

View File

@ -0,0 +1,48 @@
package net.corda.kotlin.rpc
import com.typesafe.config.*
import net.corda.core.crypto.commonName
import net.corda.core.identity.Party
import net.corda.nodeapi.User
class NodeConfig(
val party: Party,
val p2pPort: Int,
val rpcPort: Int,
val webPort: Int,
val extraServices: List<String>,
val users: List<User>,
var networkMap: NodeConfig? = null
) {
companion object {
val renderOptions: ConfigRenderOptions = ConfigRenderOptions.defaults().setOriginComments(false)
}
val commonName: String = party.name.commonName
/*
* The configuration object depends upon the networkMap,
* which is mutable.
*/
fun toFileConfig(): Config = ConfigFactory.empty()
.withValue("myLegalName", valueFor(party.name.toString()))
.withValue("p2pAddress", addressValueFor(p2pPort))
.withValue("extraAdvertisedServiceIds", valueFor(extraServices))
.withFallback(optional("networkMapService", networkMap, { c, n ->
c.withValue("address", addressValueFor(n.p2pPort))
.withValue("legalName", valueFor(n.party.name.toString()))
}))
.withValue("webAddress", addressValueFor(webPort))
.withValue("rpcAddress", addressValueFor(rpcPort))
.withValue("rpcUsers", valueFor(users.map(User::toMap).toList()))
.withValue("useTestClock", valueFor(true))
fun toText(): String = toFileConfig().root().render(renderOptions)
private fun <T> valueFor(any: T): ConfigValue? = ConfigValueFactory.fromAnyRef(any)
private fun addressValueFor(port: Int) = valueFor("localhost:$port")
private inline fun <T> 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)
}
}

View File

@ -0,0 +1,107 @@
package net.corda.kotlin.rpc
import com.google.common.net.HostAndPort
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCConnection
import net.corda.core.utilities.loggerFor
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit.SECONDS
import kotlin.test.*
class NodeProcess(
val config: NodeConfig,
val nodeDir: Path,
private val node: Process,
private val client: CordaRPCClient
) : AutoCloseable {
private companion object {
val log = loggerFor<NodeProcess>()
val javaPath: Path = Paths.get(System.getProperty("java.home"), "bin", "java")
val corda = File(this::class.java.getResource("/corda.jar").toURI())
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
val capsuleDir: Path = buildDir.resolve("capsule")
}
fun connect(): CordaRPCConnection {
val user = config.users[0]
return client.start(user.username, user.password)
}
override fun close() {
log.info("Stopping node '${config.commonName}'")
node.destroy()
if (!node.waitFor(60, SECONDS)) {
log.warn("Node '${config.commonName}' has not shutdown correctly")
node.destroyForcibly()
}
log.info("Deleting Artemis directories, because they're large!")
nodeDir.resolve("artemis").toFile().deleteRecursively()
}
class Factory(val nodesDir: Path) {
init {
assertTrue(nodesDir.toFile().forceDirectory(), "Directory '$nodesDir' does not exist")
}
fun create(config: NodeConfig): NodeProcess {
val nodeDir = Files.createTempDirectory(nodesDir, config.commonName)
log.info("Node directory: {}", nodeDir)
val confFile = nodeDir.resolve("node.conf").toFile()
confFile.writeText(config.toText())
val process = startNode(nodeDir)
val client = CordaRPCClient(HostAndPort.fromParts("localhost", config.rpcPort))
val user = config.users[0]
val setupExecutor = Executors.newSingleThreadScheduledExecutor()
try {
setupExecutor.scheduleWithFixedDelay({
try {
if (!process.isAlive) {
log.error("Node '${config.commonName}' has died.")
return@scheduleWithFixedDelay
}
val conn = client.start(user.username, user.password)
conn.close()
// Cancel the "setup" task now that we've created the RPC client.
setupExecutor.shutdown()
} catch (e: Exception) {
log.warn("Node '{}' not ready yet (Error: {})", config.commonName, e.message)
}
}, 5, 1, SECONDS)
val setupOK = setupExecutor.awaitTermination(120, SECONDS)
assertTrue(setupOK && process.isAlive, "Failed to create RPC connection")
} catch (e: Exception) {
process.destroyForcibly()
throw e
} finally {
setupExecutor.shutdownNow()
}
return NodeProcess(config, nodeDir, process, client)
}
private fun startNode(nodeDir: Path): Process {
val builder = ProcessBuilder()
.command(javaPath.toString(), "-jar", corda.path)
.directory(nodeDir.toFile())
builder.environment().putAll(mapOf(
"CAPSULE_CACHE_DIR" to capsuleDir.toString()
))
return builder.start()
}
}
}
private fun File.forceDirectory(): Boolean = this.isDirectory || this.mkdirs()

View File

@ -0,0 +1,158 @@
package net.corda.kotlin.rpc
import java.io.FilterInputStream
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration.ofSeconds
import java.util.Currency
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.*
import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.notUsed
import net.corda.core.contracts.*
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.startTrackedFlow
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.sizedInputStreamAndHash
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor
import net.corda.flows.CashIssueFlow
import net.corda.nodeapi.User
import org.junit.After
import org.junit.Before
import org.junit.Test
class StandaloneCordaRPClientTest {
private companion object {
val log = loggerFor<StandaloneCordaRPClientTest>()
val buildDir: Path = Paths.get(System.getProperty("build.dir"))
val nodesDir: Path = buildDir.resolve("nodes")
val user = User("user1", "test", permissions = setOf("ALL"))
val factory = NodeProcess.Factory(nodesDir)
val port = AtomicInteger(15000)
const val attachmentSize = 2116
const val timeout = 60L
}
private lateinit var notary: NodeProcess
private lateinit var rpcProxy: CordaRPCOps
private lateinit var connection: CordaRPCConnection
private lateinit var notaryIdentity: Party
private val notaryConfig = NodeConfig(
party = DUMMY_NOTARY,
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
webPort = port.andIncrement,
extraServices = listOf("corda.notary.validating"),
users = listOf(user)
)
@Before
fun setUp() {
notary = factory.create(notaryConfig)
connection = notary.connect()
rpcProxy = connection.proxy
notaryIdentity = fetchNotaryIdentity()
}
@After
fun done() {
try {
connection.close()
} finally {
notary.close()
}
}
@Test
fun `test attachment upload`() {
val attachment = sizedInputStreamAndHash(attachmentSize)
assertFalse(rpcProxy.attachmentExists(attachment.sha256))
val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) }
assertEquals(id, attachment.sha256, "Attachment has incorrect SHA256 hash")
}
@Test
fun `test starting flow`() {
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
}
@Test
fun `test starting tracked flow`() {
var trackCount = 0
val handle = rpcProxy.startTrackedFlow(
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity
)
handle.progress.subscribe { msg ->
log.info("Flow>> $msg")
++trackCount
}
handle.returnValue.getOrThrow(ofSeconds(timeout))
assertNotEquals(0, trackCount)
}
@Test
fun `test network map`() {
assertEquals(DUMMY_NOTARY.name, notaryIdentity.name)
}
@Test
fun `test state machines`() {
val (stateMachines, updates) = rpcProxy.stateMachinesAndUpdates()
assertEquals(0, stateMachines.size)
var updateCount = 0
updates.subscribe { update ->
if (update is StateMachineUpdate.Added) {
log.info("StateMachine>> Id=${update.id}")
++updateCount
}
}
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
assertEquals(1, updateCount)
}
@Test
fun `test vault`() {
val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates()
assertEquals(0, vault.size)
var updateCount = 0
vaultUpdates.subscribe { update ->
log.info("Vault>> FlowId=${update.flowId}")
++updateCount
}
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
assertNotEquals(0, updateCount)
// Check that this cash exists in the vault
val cashBalance = rpcProxy.getCashBalances()
log.info("Cash Balances: $cashBalance")
assertEquals(1, cashBalance.size)
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
}
private fun fetchNotaryIdentity(): Party {
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates()
nodeUpdates.notUsed()
assertEquals(1, nodeInfo.size)
return nodeInfo[0].legalIdentity
}
// This InputStream cannot have been whitelisted.
private class WrapperStream(input: InputStream) : FilterInputStream(input)
}

View File

@ -47,8 +47,8 @@ open class AbstractRPCTest {
}.get() }.get()
RPCTestMode.Netty -> RPCTestMode.Netty ->
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server -> startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
startRpcClient<I>(server.hostAndPort, rpcUser.username, rpcUser.password, clientConfiguration).map { startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
TestProxy(it, { startArtemisSession(server.hostAndPort, rpcUser.username, rpcUser.password) }) TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
} }
}.get() }.get()
} }

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=0.12.0 gradlePluginsVersion=0.12.1
kotlinVersion=1.1.2 kotlinVersion=1.1.2
guavaVersion=21.0 guavaVersion=21.0
bouncycastleVersion=1.56 bouncycastleVersion=1.56

View File

@ -0,0 +1,15 @@
apply plugin: 'java'
apply plugin: 'maven-publish'
apply plugin: 'net.corda.plugins.publish-utils'
repositories {
mavenCentral()
}
dependencies {
// TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version"
// Bouncy Castle: for X.500 distinguished name manipulation
compile "org.bouncycastle:bcprov-jdk15on:$bouncycastle_version"
}

View File

@ -0,0 +1,8 @@
package net.corda.cordform;
import org.bouncycastle.asn1.x500.X500Name;
import java.nio.file.Path;
public interface CordformContext {
Path baseDirectory(X500Name nodeName);
}

View File

@ -0,0 +1,27 @@
package net.corda.cordform;
import org.bouncycastle.asn1.x500.X500Name;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.function.Consumer;
public abstract class CordformDefinition {
public final Path driverDirectory;
public final ArrayList<Consumer<? super CordformNode>> nodeConfigurers = new ArrayList<>();
public final X500Name networkMapNodeName;
public CordformDefinition(Path driverDirectory, X500Name networkMapNodeName) {
this.driverDirectory = driverDirectory;
this.networkMapNodeName = networkMapNodeName;
}
public void addNode(Consumer<? super CordformNode> configurer) {
nodeConfigurers.add(configurer);
}
/**
* Make arbitrary changes to the node directories before they are started.
* @param context Lookup of node directory by node name.
*/
public abstract void setup(CordformContext context);
}

View File

@ -0,0 +1,92 @@
package net.corda.cordform;
import static java.util.Collections.emptyList;
import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import com.typesafe.config.ConfigValueFactory;
import java.util.List;
import java.util.Map;
public class CordformNode {
protected static final String DEFAULT_HOST = "localhost";
/**
* Name of the node.
*/
private String name;
public String getName() {
return name;
}
/**
* A list of advertised services ID strings.
*/
public List<String> advertisedServices = emptyList();
/**
* If running a distributed notary, a list of node addresses for joining the Raft cluster
*/
public List<String> notaryClusterAddresses = emptyList();
/**
* Set the RPC users for this node. This configuration block allows arbitrary configuration.
* The recommended current structure is:
* [[['username': "username_here", 'password': "password_here", 'permissions': ["permissions_here"]]]
* The above is a list to a map of keys to values using Groovy map and list shorthands.
*
* Incorrect configurations will not cause a DSL error.
*/
public List<Map<String, Object>> rpcUsers = emptyList();
protected Config config = ConfigFactory.empty();
public Config getConfig() {
return config;
}
/**
* Set the name of the node.
*
* @param name The node name.
*/
public void name(String name) {
this.name = name;
config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name));
}
/**
* Set the nearest city to the node.
*
* @param nearestCity The name of the nearest city to the node.
*/
public void nearestCity(String nearestCity) {
config = config.withValue("nearestCity", ConfigValueFactory.fromAnyRef(nearestCity));
}
/**
* Set the Artemis P2P port for this node.
*
* @param p2pPort The Artemis messaging queue port.
*/
public void p2pPort(Integer p2pPort) {
config = config.withValue("p2pAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + p2pPort));
}
/**
* Set the Artemis RPC port for this node.
*
* @param rpcPort The Artemis RPC queue port.
*/
public void rpcPort(Integer rpcPort) {
config = config.withValue("rpcAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + rpcPort));
}
/**
* Set the port which to bind the Copycat (Raft) node to
*
* @param notaryPort The Raft port.
*/
public void notaryNodePort(Integer notaryPort) {
config = config.withValue("notaryNodeAddress", ConfigValueFactory.fromAnyRef(DEFAULT_HOST + ':' + notaryPort));
}
}

View File

@ -63,7 +63,7 @@ dependencies {
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}" compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/ // Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
compile 'net.i2p.crypto:eddsa:0.2.0' compile "net.i2p.crypto:eddsa:$eddsa_version"
// Bouncy castle support needed for X509 certificate manipulation // Bouncy castle support needed for X509 certificate manipulation
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}" compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"

View File

@ -9,6 +9,7 @@ import com.google.common.util.concurrent.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable import rx.Observable
@ -32,13 +33,7 @@ import java.util.zip.Deflater
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream import java.util.zip.ZipOutputStream
import kotlin.collections.Iterable
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
import kotlin.collections.List
import kotlin.collections.filter
import kotlin.collections.firstOrNull
import kotlin.collections.fold
import kotlin.collections.forEach
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -113,8 +108,17 @@ infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) } infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe. @Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) }) infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) } infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
inline fun <T, reified R> Collection<T>.mapToArray(transform: (T) -> R) = run {
val iterator = iterator()
var expected = 0
Array(size) {
expected++ == it || throw UnsupportedOperationException("Array constructor is non-sequential!")
transform(iterator.next())
}
}
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */ /** Executes the given block and sets the future to either the result, or any exception that was thrown. */
inline fun <T> SettableFuture<T>.catch(block: () -> T) { inline fun <T> SettableFuture<T>.catch(block: () -> T) {
try { try {
@ -136,7 +140,8 @@ fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
} }
/** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */ /** Allows you to write code like: Paths.get("someDir") / "subdir" / "filename" but using the Paths API to avoid platform separator problems. */
operator fun Path.div(other: String): Path = resolve(other) operator fun Path.div(other: String) = resolve(other)
operator fun String.div(other: String) = Paths.get(this) / other
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs) fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs) fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
@ -271,7 +276,7 @@ class ThreadBox<out T>(val content: T, val lock: ReentrantLock = ReentrantLock()
* We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization. * We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization.
*/ */
@CordaSerializable @CordaSerializable
abstract class RetryableException(message: String) : Exception(message) abstract class RetryableException(message: String) : FlowException(message)
/** /**
* A simple wrapper that enables the use of Kotlin's "val x by TransientProperty { ... }" syntax. Such a property * A simple wrapper that enables the use of Kotlin's "val x by TransientProperty { ... }" syntax. Such a property

View File

@ -56,6 +56,7 @@ infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantit
//// Requirements ///////////////////////////////////////////////////////////////////////////////////////////////////// //// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
object Requirements { object Requirements {
/** Throws [IllegalArgumentException] if the given expression evaluates to false. */
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI. @Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
infix inline fun String.using(expr: Boolean) { infix inline fun String.using(expr: Boolean) {
if (!expr) throw IllegalArgumentException("Failed requirement: $this") if (!expr) throw IllegalArgumentException("Failed requirement: $this")
@ -93,13 +94,14 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
filter { if (parties == null) true else it.signingParties.containsAll(parties) }. filter { if (parties == null) true else it.signingParties.containsAll(parties) }.
map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) } map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) }
/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand() = try { inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
select<T>().single() select<T>().single()
} catch (e: NoSuchElementException) { } catch (e: NoSuchElementException) {
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message. throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
} }
// For Java /** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */
fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) = fun <C : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand(klass: Class<C>) =
mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single() mapNotNull { @Suppress("UNCHECKED_CAST") if (klass.isInstance(it.value)) it as AuthenticatedObject<C> else null }.single()
@ -115,7 +117,7 @@ inline fun <reified T : MoveCommand> verifyMoveCommand(inputs: List<OwnableState
// Now check the digital signatures on the move command. Every input has an owning public key, and we must // Now check the digital signatures on the move command. Every input has an owning public key, and we must
// see a signature from each of those keys. The actual signatures have been verified against the transaction // see a signature from each of those keys. The actual signatures have been verified against the transaction
// data by the platform before execution. // data by the platform before execution.
val owningPubKeys = inputs.map { it.owner }.toSet() val owningPubKeys = inputs.map { it.owner.owningKey }.toSet()
val command = commands.requireSingleCommand<T>() val command = commands.requireSingleCommand<T>()
val keysThatSigned = command.signers.toSet() val keysThatSigned = command.signers.toSet()
requireThat { requireThat {

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes. // The dummy contract doesn't do anything useful. It exists for testing purposes.
@ -14,12 +14,12 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
val magicNumber: Int val magicNumber: Int
} }
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State { data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: AbstractParty) : OwnableState, State {
override val contract = DUMMY_PROGRAM_ID override val contract = DUMMY_PROGRAM_ID
override val participants: List<PublicKey> override val participants: List<AbstractParty>
get() = listOf(owner) get() = listOf(owner)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner))
} }
/** /**
@ -28,9 +28,9 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
* in a different field, however this is a good example of a contract with multiple states. * in a different field, however this is a good example of a contract with multiple states.
*/ */
data class MultiOwnerState(override val magicNumber: Int = 0, data class MultiOwnerState(override val magicNumber: Int = 0,
val owners: List<PublicKey>) : ContractState, State { val owners: List<AbstractParty>) : ContractState, State {
override val contract = DUMMY_PROGRAM_ID override val contract = DUMMY_PROGRAM_ID
override val participants: List<PublicKey> get() = owners override val participants: List<AbstractParty> get() = owners
} }
interface Commands : CommandData { interface Commands : CommandData {
@ -47,22 +47,22 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder { fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder {
val owners = listOf(owner) + otherOwners val owners = listOf(owner) + otherOwners
return if (owners.size == 1) { return if (owners.size == 1) {
val state = SingleOwnerState(magicNumber, owners.first().party.owningKey) val state = SingleOwnerState(magicNumber, owners.first().party)
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey)) TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey))
} else { } else {
val state = MultiOwnerState(magicNumber, owners.map { it.party.owningKey }) val state = MultiOwnerState(magicNumber, owners.map { it.party })
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey })) TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey }))
} }
} }
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner) fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: AbstractParty) = move(listOf(prior), newOwner)
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder { fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: AbstractParty): TransactionBuilder {
require(priors.isNotEmpty()) require(priors.isNotEmpty())
val priorState = priors[0].state.data val priorState = priors[0].state.data
val (cmd, state) = priorState.withNewOwner(newOwner) val (cmd, state) = priorState.withNewOwner(newOwner)
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems( return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
/* INPUTS */ *priors.toTypedArray(), /* INPUTS */ *priors.toTypedArray(),
/* COMMAND */ Command(cmd, priorState.owner), /* COMMAND */ Command(cmd, priorState.owner.owningKey),
/* OUTPUT */ state /* OUTPUT */ state
) )
} }

View File

@ -1,9 +1,9 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.flows.ContractUpgradeFlow import net.corda.flows.ContractUpgradeFlow
import java.security.PublicKey
// The dummy contract doesn't do anything useful. It exists for testing purposes. // The dummy contract doesn't do anything useful. It exists for testing purposes.
val DUMMY_V2_PROGRAM_ID = DummyContractV2() val DUMMY_V2_PROGRAM_ID = DummyContractV2()
@ -15,9 +15,9 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> { class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
override val legacyContract = DummyContract::class.java override val legacyContract = DummyContract::class.java
data class State(val magicNumber: Int = 0, val owners: List<PublicKey>) : ContractState { data class State(val magicNumber: Int = 0, val owners: List<AbstractParty>) : ContractState {
override val contract = DUMMY_V2_PROGRAM_ID override val contract = DUMMY_V2_PROGRAM_ID
override val participants: List<PublicKey> = owners override val participants: List<AbstractParty> = owners
} }
interface Commands : CommandData { interface Commands : CommandData {
@ -44,16 +44,16 @@ class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.St
* *
* @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid. * @return a pair of wire transaction, and a set of those who should sign the transaction for it to be valid.
*/ */
fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<PublicKey>> { fun generateUpgradeFromV1(vararg states: StateAndRef<DummyContract.State>): Pair<WireTransaction, Set<AbstractParty>> {
val notary = states.map { it.state.notary }.single() val notary = states.map { it.state.notary }.single()
require(states.isNotEmpty()) require(states.isNotEmpty())
val signees = states.flatMap { it.state.data.participants }.toSet() val signees: Set<AbstractParty> = states.flatMap { it.state.data.participants }.distinct().toSet()
return Pair(TransactionType.General.Builder(notary).apply { return Pair(TransactionType.General.Builder(notary).apply {
states.forEach { states.forEach {
addInputState(it) addInputState(it)
addOutputState(upgrade(it.state.data)) addOutputState(upgrade(it.state.data))
addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.toList()) addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList())
} }
}.toWireTransaction(), signees) }.toWireTransaction(), signees)
} }

View File

@ -1,12 +1,12 @@
package net.corda.core.contracts package net.corda.core.contracts
import java.security.PublicKey import net.corda.core.identity.AbstractParty
/** /**
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract]. * Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
*/ */
data class DummyState(val magicNumber: Int = 0) : ContractState { data class DummyState(val magicNumber: Int = 0) : ContractState {
override val contract = DUMMY_PROGRAM_ID override val contract = DUMMY_PROGRAM_ID
override val participants: List<PublicKey> override val participants: List<AbstractParty>
get() = emptyList() get() = emptyList()
} }

View File

@ -1,12 +1,8 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.identity.Party
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.transactions.TransactionBuilder import net.corda.core.identity.AbstractParty
import net.corda.core.utilities.loggerFor
import net.corda.core.utilities.trace
import java.security.PublicKey import java.security.PublicKey
import java.util.*
class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing") class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing")
@ -32,9 +28,9 @@ interface FungibleAsset<T : Any> : OwnableState {
*/ */
val exitKeys: Collection<PublicKey> val exitKeys: Collection<PublicKey>
/** There must be a MoveCommand signed by this key to claim the amount */ /** There must be a MoveCommand signed by this key to claim the amount */
override val owner: PublicKey override val owner: AbstractParty
fun move(newAmount: Amount<Issued<T>>, newOwner: PublicKey): FungibleAsset<T> fun move(newAmount: Amount<Issued<T>>, newOwner: AbstractParty): FungibleAsset<T>
// Just for grouping // Just for grouping
interface Commands : CommandData { interface Commands : CommandData {

View File

@ -4,6 +4,7 @@ import net.corda.core.contracts.clauses.Clause
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogicRef import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
@ -114,7 +115,7 @@ interface ContractState {
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants * The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants
* list should just contain the owner. * list should just contain the owner.
*/ */
val participants: List<PublicKey> val participants: List<AbstractParty>
} }
/** /**
@ -174,10 +175,10 @@ fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, to
*/ */
interface OwnableState : ContractState { interface OwnableState : ContractState {
/** There must be a MoveCommand signed by this key to claim the amount */ /** There must be a MoveCommand signed by this key to claim the amount */
val owner: PublicKey val owner: AbstractParty
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */ /** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState> fun withNewOwner(newOwner: AbstractParty): Pair<CommandData, OwnableState>
} }
/** Something which is scheduled to happen at a point in time */ /** Something which is scheduled to happen at a point in time */
@ -280,7 +281,7 @@ interface DealState : LinearState {
* separate process exchange certificates to ascertain identities. Thus decoupling identities from * separate process exchange certificates to ascertain identities. Thus decoupling identities from
* [ContractState]s. * [ContractState]s.
* */ * */
val parties: List<AnonymousParty> val parties: List<AbstractParty>
/** /**
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general * Generate a partial transaction representing an agreement (command) to this deal, allowing a general
@ -343,9 +344,7 @@ inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filt
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party. * ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party.
*/ */
@CordaSerializable @CordaSerializable
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) { data class PartyAndReference(val party: AbstractParty, val reference: OpaqueBytes) {
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
override fun toString() = "$party$reference" override fun toString() = "$party$reference"
} }
@ -411,7 +410,12 @@ data class AuthenticatedObject<out T : Any>(
* between (after, before). * between (after, before).
*/ */
@CordaSerializable @CordaSerializable
data class Timestamp(val after: Instant?, val before: Instant?) { data class Timestamp(
/** The time at which this transaction is said to have occurred is after this moment */
val after: Instant?,
/** The time at which this transaction is said to have occurred is before this moment */
val before: Instant?
) {
init { init {
if (after == null && before == null) if (after == null && before == null)
throw IllegalArgumentException("At least one of before/after must be specified") throw IllegalArgumentException("At least one of before/after must be specified")

View File

@ -2,6 +2,7 @@ package net.corda.core.contracts
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
@ -60,7 +61,7 @@ sealed class TransactionType {
abstract fun verifyTransaction(tx: LedgerTransaction) abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */ /** A general transaction type where transaction validity is determined by custom contract code */
object General : TransactionType() { object General : TransactionType(), DeserializeAsKotlinObjectDef {
/** Just uses the default [TransactionBuilder] with no special logic */ /** Just uses the default [TransactionBuilder] with no special logic */
class Builder(notary: Party?) : TransactionBuilder(General, notary) class Builder(notary: Party?) : TransactionBuilder(General, notary)
@ -140,14 +141,14 @@ sealed class TransactionType {
* A special transaction type for reassigning a notary for a state. Validation does not involve running * A special transaction type for reassigning a notary for a state. Validation does not involve running
* any contract code, it just checks that the states are unmodified apart from the notary field. * any contract code, it just checks that the states are unmodified apart from the notary field.
*/ */
object NotaryChange : TransactionType() { object NotaryChange : TransactionType(), DeserializeAsKotlinObjectDef {
/** /**
* A transaction builder that automatically sets the transaction type to [NotaryChange] * A transaction builder that automatically sets the transaction type to [NotaryChange]
* and adds the list of participants to the signers set for every input state. * and adds the list of participants to the signers set for every input state.
*/ */
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) { class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
override fun addInputState(stateAndRef: StateAndRef<*>) { override fun addInputState(stateAndRef: StateAndRef<*>) {
signers.addAll(stateAndRef.state.data.participants) signers.addAll(stateAndRef.state.data.participants.map { it.owningKey })
super.addInputState(stateAndRef) super.addInputState(stateAndRef)
} }
} }
@ -170,6 +171,6 @@ sealed class TransactionType {
} }
} }
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet() override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
} }
} }

View File

@ -1,6 +1,8 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.security.* import java.security.*
import java.security.spec.AlgorithmParameterSpec import java.security.spec.AlgorithmParameterSpec
@ -10,7 +12,12 @@ import java.security.spec.AlgorithmParameterSpec
*/ */
class CompositeSignature : Signature(ALGORITHM) { class CompositeSignature : Signature(ALGORITHM) {
companion object { companion object {
val ALGORITHM = "X-Corda-CompositeSig" val ALGORITHM = "2.25.30086077608615255153862931087626791003"
// UUID-based OID
// TODO: Register for an OID space and issue our own shorter OID
val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM))
fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
} }
private var signatureState: State? = null private var signatureState: State? = null

View File

@ -21,13 +21,20 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
import org.bouncycastle.jce.ECNamedCurveTable import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.pkcs.PKCS10CertificationRequest import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
import sun.security.pkcs.PKCS8Key
import sun.security.util.DerValue
import sun.security.x509.X509Key
import java.math.BigInteger import java.math.BigInteger
import java.security.* import java.security.*
import java.security.KeyFactory import java.security.KeyFactory
@ -140,6 +147,10 @@ object Crypto {
SPHINCS256_SHA256 SPHINCS256_SHA256
).associateBy { it.schemeCodeName } ).associateBy { it.schemeCodeName }
// We need to group signature schemes per algorithm, so to quickly identify them during decoding.
// Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256.
private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName }
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
// that could cause unexpected and suspicious behaviour. // that could cause unexpected and suspicious behaviour.
// i.e. if someone removes a Provider and then he/she adds a new one with the same name. // i.e. if someone removes a Provider and then he/she adds a new one with the same name.
@ -167,37 +178,20 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
*/ */
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName") fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
/** /**
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key]. * Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
* This function is usually called when requiring to verify signatures and the signing schemes must be defined. * This function is usually called when requiring to verify signatures and the signing schemes must be defined.
* Note that only the Corda platform standard schemes are supported (see [Crypto]). * For the supported signature schemes see [Crypto].
* Note that we always need to add an additional if-else statement when there are signature schemes
* with the same algorithmName, but with different parameters (e.g. now there are two ECDSA schemes, each using its own curve).
* @param key either private or public. * @param key either private or public.
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported. * @throws IllegalArgumentException if the requested key type is not supported.
*/ */
fun findSignatureScheme(key: Key): SignatureScheme { fun findSignatureScheme(key: Key): SignatureScheme {
for (sig in supportedSignatureSchemes.values) { val algorithm = matchingAlgorithmName(key.algorithm)
var algorithm = key.algorithm algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it }
if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC. throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format")
if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256.
if (algorithm == sig.algorithmName) {
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters.
if (algorithm == "EdDSA") {
if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) {
return sig
} else break // use continue if in the future we support more than one Edwards curves.
} else if (algorithm == "ECDSA") {
if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) {
return sig
} else continue
} else return sig // it's either RSA_SHA256 or SPHINCS-256.
}
}
throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}")
} }
/** /**
@ -209,11 +203,16 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm)
// There are cases where the same key algorithm is applied to different signature schemes.
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
// to generate the appropriate key.
for (signatureScheme in algorithmGroups[algorithm]!!) {
try { try {
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception. // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
} }
} }
throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.") throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.")
@ -240,6 +239,8 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey { fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
try { try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
@ -256,11 +257,16 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePublicKey(encodedKey: ByteArray): PublicKey { fun decodePublicKey(encodedKey: ByteArray): PublicKey {
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) { val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm)
// There are cases where the same key algorithm is applied to different signature schemes.
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
// to generate the appropriate key.
for (signatureScheme in algorithmGroups[algorithm]!!) {
try { try {
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception. // ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
} }
} }
throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.") throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.")
@ -271,7 +277,7 @@ object Crypto {
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256). * @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey an X509 encoded public key. * @param encodedKey an X509 encoded public key.
* @throws IllegalArgumentException if the requested scheme is not supported * @throws IllegalArgumentException if the requested scheme is not supported.
* @throws InvalidKeySpecException if the given key specification * @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key. * is inappropriate for this key factory to produce a public key.
*/ */
@ -283,12 +289,14 @@ object Crypto {
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers. * This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256). * @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
* @param encodedKey an X509 encoded public key. * @param encodedKey an X509 encoded public key.
* @throws IllegalArgumentException if the requested scheme is not supported * @throws IllegalArgumentException if the requested scheme is not supported.
* @throws InvalidKeySpecException if the given key specification * @throws InvalidKeySpecException if the given key specification
* is inappropriate for this key factory to produce a public key. * is inappropriate for this key factory to produce a public key.
*/ */
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class) @Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey { fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
try { try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey)) return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) { } catch (ikse: InvalidKeySpecException) {
@ -334,7 +342,7 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray { fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!") if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
@ -414,7 +422,7 @@ object Crypto {
*/ */
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!") if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!") if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
@ -440,7 +448,7 @@ object Crypto {
*/ */
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class) @Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean { fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean {
if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.encoded.toBase58()} does not match the input clearData: ${publicKey.encoded.toBase58()}") if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.toStringShort()} does not match")
return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes()) return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes())
} }
@ -477,7 +485,7 @@ object Crypto {
*/ */
@Throws(SignatureException::class, IllegalArgumentException::class) @Throws(SignatureException::class, IllegalArgumentException::class)
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean { fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName]) val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
signature.initVerify(publicKey) signature.initVerify(publicKey)
@ -505,7 +513,7 @@ object Crypto {
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
@JvmOverloads @JvmOverloads
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair { fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName)) if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName") throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]) val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
if (signatureScheme.algSpec != null) if (signatureScheme.algSpec != null)
@ -547,20 +555,16 @@ object Crypto {
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv)) return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
} }
/** Check if the requested signature scheme is supported by the system. */
fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes
/** /**
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key * Use bouncy castle utilities to sign completed X509 certificate with CA cert private key.
*/ */
fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair, fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair,
subject: X500Name, subjectPublicKey: PublicKey, subject: X500Name, subjectPublicKey: PublicKey,
keyUsage: KeyUsage, purposes: List<KeyPurposeId>, keyUsage: KeyUsage, purposes: List<KeyPurposeId>,
signatureScheme: SignatureScheme, validityWindow: Pair<Date, Date>, validityWindow: Pair<Date, Date>,
pathLength: Int? = null, subjectAlternativeName: List<GeneralName>? = null): X509Certificate { pathLength: Int? = null, subjectAlternativeName: List<GeneralName>? = null): X509Certificate {
val signatureScheme = findSignatureScheme(issuerKeyPair.private)
val provider = providerMap[signatureScheme.providerName] val provider = providerMap[signatureScheme.providerName]
val serial = BigInteger.valueOf(random63BitValue()) val serial = BigInteger.valueOf(random63BitValue())
val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } }) val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } })
@ -598,9 +602,9 @@ object Crypto {
* Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks. * Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks.
* Point-at-infinity is not permitted as well. * Point-at-infinity is not permitted as well.
* @see <a href="https://safecurves.cr.yp.to/twist.html">Small subgroup and invalid-curve attacks</a> for a more descriptive explanation on such attacks. * @see <a href="https://safecurves.cr.yp.to/twist.html">Small subgroup and invalid-curve attacks</a> for a more descriptive explanation on such attacks.
* We use this function on [findSignatureScheme] for a [PublicKey]; currently used for signature verification only. * We use this function on [validatePublicKey], which is currently used for signature verification only.
* Thus, as these attacks are mostly not relevant to signature verification, we should note that * Thus, as these attacks are mostly not relevant to signature verification, we should note that
* we're doing it out of an abundance of caution and specifically to proactively protect developers * we are doing it out of an abundance of caution and specifically to proactively protect developers
* against using these points as part of a DH key agreement or for use cases as yet unimagined. * against using these points as part of a DH key agreement or for use cases as yet unimagined.
* This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve). * This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve).
* @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve. * @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve.
@ -622,4 +626,69 @@ object Crypto {
// return true if EdDSA publicKey is point at infinity. // return true if EdDSA publicKey is point at infinity.
// For EdDSA a custom function is required as it is not supported by the I2P implementation. // For EdDSA a custom function is required as it is not supported by the I2P implementation.
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3) private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
/** Check if the requested [SignatureScheme] is supported by the system. */
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
// map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names.
private fun matchingAlgorithmName(algorithm: String): String {
return when (algorithm) {
"EC" -> "ECDSA"
"SPHINCS-256" -> "SPHINCS256"
"1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256.
else -> algorithm
}
}
// validate a key, by checking its algorithmic params.
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
return when (key) {
is PublicKey -> validatePublicKey(signatureScheme, key)
is PrivateKey -> validatePrivateKey(signatureScheme, key)
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
}
}
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
when (key) {
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key)
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
}
}
// check if a private key satisfies algorithm specs.
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
when (key) {
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
}
}
/**
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.
* @param key a public key.
* @return a supported implementation of the input public key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPublicKey(key: PublicKey): PublicKey {
return Crypto.decodePublicKey(key.encoded)
}
/**
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
* This method is usually required to retrieve keys from JKS keystores that by default return SUN implementations.
* @param key a private key.
* @return a supported implementation of the input private key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
return Crypto.decodePrivateKey(key.encoded)
}
} }

View File

@ -2,10 +2,11 @@
package net.corda.core.crypto 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.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.i2p.crypto.eddsa.EdDSAPublicKey
import java.math.BigInteger import java.math.BigInteger
import net.corda.core.utilities.SgxSupport import net.corda.core.utilities.SgxSupport
import java.security.* import java.security.*
@ -19,6 +20,8 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun toString() = "NULL_KEY" override fun toString() = "NULL_KEY"
} }
val NULL_PARTY = AnonymousParty(NullPublicKey)
// TODO: Clean up this duplication between Null and Dummy public key // TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable @CordaSerializable
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> { class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
@ -69,11 +72,9 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.byte
// implementation of CompositeSignature. // implementation of CompositeSignature.
@Throws(InvalidKeyException::class) @Throws(InvalidKeyException::class)
fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable { fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
// Quick workaround when we have CompositeKey as Party owningKey.
if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
val sig = sign(bytesToSign) val sig = sign(bytesToSign)
val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey.
is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
else -> party.owningKey
}
return DigitalSignature.LegallyIdentifiable(party, sig.bytes) return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
} }

View File

@ -16,10 +16,10 @@ object KeyStoreUtilities {
/** /**
* Helper method to either open an existing keystore for modification, or create a new blank keystore. * Helper method to either open an existing keystore for modification, or create a new blank keystore.
* @param keyStoreFilePath location of KeyStore file * @param keyStoreFilePath location of KeyStore file.
* @param storePassword password to open the store. This does not have to be the same password as any keys stored, * @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
* @return returns the KeyStore opened/created * @return returns the KeyStore opened/created.
*/ */
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore { fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
val pass = storePassword.toCharArray() val pass = storePassword.toCharArray()
@ -34,11 +34,11 @@ object KeyStoreUtilities {
} }
/** /**
* Helper method to open an existing keystore for modification/read * Helper method to open an existing keystore for modification/read.
* @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException * @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException.
* @param storePassword password to open the store. This does not have to be the same password as any keys stored, * @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
* @return returns the KeyStore opened * @return returns the KeyStore opened.
* @throws IOException if there was an error reading the key store from the file. * @throws IOException if there was an error reading the key store from the file.
* @throws KeyStoreException if the password is incorrect or the key store is damaged. * @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/ */
@ -48,11 +48,11 @@ object KeyStoreUtilities {
} }
/** /**
* Helper method to open an existing keystore for modification/read * Helper method to open an existing keystore for modification/read.
* @param input stream containing a KeyStore e.g. loaded from a resource file * @param input stream containing a KeyStore e.g. loaded from a resource file.
* @param storePassword password to open the store. This does not have to be the same password as any keys stored, * @param storePassword password to open the store. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
* @return returns the KeyStore opened * @return returns the KeyStore opened.
* @throws IOException if there was an error reading the key store from the stream. * @throws IOException if there was an error reading the key store from the stream.
* @throws KeyStoreException if the password is incorrect or the key store is damaged. * @throws KeyStoreException if the password is incorrect or the key store is damaged.
*/ */
@ -68,12 +68,12 @@ object KeyStoreUtilities {
} }
/** /**
* Helper extension method to add, or overwrite any key data in store * 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 alias name to record the private key and certificate chain under.
* @param key cryptographic key to store * @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, * @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. * but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert * @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert.
*/ */
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) { fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) {
if (containsAlias(alias)) { if (containsAlias(alias)) {
@ -83,9 +83,9 @@ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain
} }
/** /**
* Helper extension method to add, or overwrite any public certificate data in store * Helper extension method to add, or overwrite any public certificate data in store.
* @param alias name to record the public certificate under * @param alias name to record the public certificate under.
* @param cert certificate to store * @param cert certificate to store.
*/ */
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) { fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
if (containsAlias(alias)) { if (containsAlias(alias)) {
@ -96,8 +96,8 @@ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
/** /**
* Helper method save KeyStore to storage * Helper method save KeyStore to storage.
* @param keyStoreFilePath the file location to save to * @param keyStoreFilePath the file location to save to.
* @param storePassword password to access the store in future. This does not have to be the same password as any keys stored, * @param storePassword password to access the store in future. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended. * but for SSL purposes this is recommended.
*/ */
@ -108,29 +108,47 @@ fun KeyStore.store(out: OutputStream, password: String) = store(out, password.to
/** /**
* Extract public and private keys from a KeyStore file assuming storage alias is known. * Extract public and private keys from a KeyStore file assuming storage alias is known.
* @param keyPassword Password to unlock the private key entries * @param alias The name to lookup the Key and Certificate chain from.
* @param alias The name to lookup the Key and Certificate chain from * @param keyPassword Password to unlock the private key entries.
* @return The KeyPair found in the KeyStore under the specified alias * @return The KeyPair found in the KeyStore under the specified alias.
*/ */
fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKey(alias, keyPassword).keyPair fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKeyPair(alias, keyPassword).keyPair
/** /**
* Helper method to load a Certificate and KeyPair from their KeyStore. * Helper method to load a Certificate and KeyPair from their KeyStore.
* The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys. * The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys.
* @param keyPassword The password for the PrivateKey (not the store access password)
* @param alias The name to search for the data. Typically if generated with the methods here this will be one of * @param alias The name to search for the data. Typically if generated with the methods here this will be one of
* CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above * CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above.
* @param keyPassword The password for the PrivateKey (not the store access password).
*/ */
fun KeyStore.getCertificateAndKey(alias: String, keyPassword: String): CertificateAndKey { fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair {
val keyPass = keyPassword.toCharArray()
val key = getKey(alias, keyPass) as PrivateKey
val cert = getCertificate(alias) as X509Certificate val cert = getCertificate(alias) as X509Certificate
return CertificateAndKey(cert, KeyPair(cert.publicKey, key)) return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword)))
} }
/** /**
* Extract public X509 certificate from a KeyStore file assuming storage alias is know * Extract public X509 certificate from a KeyStore file assuming storage alias is known.
* @param alias The name to lookup the Key and Certificate chain from * @param alias The name to lookup the Key and Certificate chain from.
* @return The X509Certificate found in the KeyStore under the specified alias * @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): X509Certificate = getCertificate(alias) as X509Certificate
/**
* Extract a private key from a KeyStore file assuming storage alias is known.
* By default, a JKS keystore returns PrivateKey implementations supported by the SUN provider.
* For instance, if one imports a BouncyCastle ECC key, JKS will return a SUN ECC key implementation on getKey.
* To convert to a supported implementation, an encode->decode method is applied to the keystore's returned object.
* @param alias The name to lookup the Key.
* @param keyPassword Password to unlock the private key entries.
* @return the requested private key in supported type.
* @throws KeyStoreException if the keystore has not been initialized.
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found (not supported from the Keystore provider).
* @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong).
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
val keyPass = keyPassword.toCharArray()
val key = getKey(alias, keyPass) as PrivateKey
return Crypto.toSupportedPrivateKey(key)
}

View File

@ -1,7 +0,0 @@
package net.corda.core.crypto
import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey
@Deprecated("Party has moved to identity package", ReplaceWith("net.corda.core.identity.Party"))
class Party(name: X500Name, owningKey: PublicKey) : net.corda.core.identity.Party(name, owningKey)

View File

@ -17,11 +17,12 @@ import java.io.FileWriter
import java.io.InputStream import java.io.InputStream
import java.net.InetAddress import java.net.InetAddress
import java.nio.file.Path import java.nio.file.Path
import java.security.InvalidAlgorithmParameterException
import java.security.KeyPair import java.security.KeyPair
import java.security.KeyStore import java.security.KeyStore
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertificateFactory import java.security.cert.*
import java.security.cert.X509Certificate import java.time.Duration
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
import java.util.* import java.util.*
@ -42,29 +43,43 @@ object X509Utilities {
private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage) private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage)
private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth) private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth)
private val DEFAULT_VALIDITY_WINDOW = Pair(0, 365 * 10) private val DEFAULT_VALIDITY_WINDOW = Pair(Duration.ofMillis(0), Duration.ofDays(365 * 10))
/** /**
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range * Helper function to return the latest out of an instant and an optional date.
* @param daysBefore number of days to roll back returned start date relative to current date
* @param daysAfter number of days to roll forward returned end date relative to current date
* @param parentNotBefore if provided is used to lower bound the date interval returned
* @param parentNotAfter if provided is used to upper bound the date interval returned
* Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date
* Thus we avoid too many round trip conversions.
*/ */
private fun getCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair<Date, Date> { private fun max(first: Instant, second: Date?): Date {
return if (second != null && second.time > first.toEpochMilli())
second
else
Date(first.toEpochMilli())
}
/**
* Helper function to return the earliest out of an instant and an optional date.
*/
private fun min(first: Instant, second: Date?): Date {
return if (second != null && second.time < first.toEpochMilli())
second
else
Date(first.toEpochMilli())
}
/**
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range.
* @param before duration to roll back returned start date relative to current date.
* @param after duration to roll forward returned end date relative to current date.
* @param parent if provided certificate whose validity should bound the date interval returned.
*/
private fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509Certificate? = null): Pair<Date, Date> {
val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS) val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS)
val notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS)).let { notBefore -> val notBefore = max(startOfDayUTC - before, parent?.notBefore)
if (parentNotBefore != null && parentNotBefore.after(notBefore)) parentNotBefore else notBefore val notAfter = min(startOfDayUTC + after, parent?.notAfter)
}
val notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS)).let { notAfter ->
if (parentNotAfter != null && parentNotAfter.after(notAfter)) parentNotAfter else notAfter
}
return Pair(notBefore, notAfter) return Pair(notBefore, notAfter)
} }
/** /**
* Return a bogus X509 for dev purposes. * Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
*/ */
@Deprecated("Full legal names should be specified in all configurations") @Deprecated("Full legal names should be specified in all configurations")
fun getDevX509Name(commonName: String): X500Name { fun getDevX509Name(commonName: String): X500Name {
@ -96,71 +111,106 @@ object X509Utilities {
/* /*
* Create a de novo root self-signed X509 v3 CA cert and [KeyPair]. * Create a de novo root self-signed X509 v3 CA cert and [KeyPair].
* @param subject the cert Subject will be populated with the domain string * @param subject the cert Subject will be populated with the domain string.
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
* @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates. * @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates * Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates.
*/ */
@JvmStatic @JvmStatic
fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair<Int, Int> = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { fun createSelfSignedCACert(subject: X500Name,
val keyPair = generateKeyPair(signatureScheme) keyPair: KeyPair,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second) val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 2) val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2)
return CertificateAndKey(cert, keyPair) return CertificateAndKeyPair(cert, keyPair)
} }
@JvmStatic
fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair
= createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow)
/** /**
* Create a de novo root intermediate X509 v3 CA cert and KeyPair. * Create a de novo root intermediate X509 v3 CA cert and KeyPair.
* @param subject subject of the generated certificate. * @param subject subject of the generated certificate.
* @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it * @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it.
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided. * @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
* @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates. * @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates * Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates.
*/ */
@JvmStatic @JvmStatic
fun createIntermediateCert(subject: X500Name, ca: CertificateAndKey, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair<Int, Int> = DEFAULT_VALIDITY_WINDOW): CertificateAndKey { fun createIntermediateCert(subject: X500Name,
ca: CertificateAndKeyPair,
signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair {
val keyPair = generateKeyPair(signatureScheme) val keyPair = generateKeyPair(signatureScheme)
val issuer = X509CertificateHolder(ca.certificate.encoded).subject val issuer = X509CertificateHolder(ca.certificate.encoded).subject
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter) val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate)
val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 1) val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1)
return CertificateAndKey(cert, keyPair) return CertificateAndKeyPair(cert, keyPair)
} }
/** /**
* Create an X509v3 certificate suitable for use in TLS roles. * Create an X509v3 certificate suitable for use in TLS roles.
* @param subject The contents to put in the subject field of the certificate * @param subject The contents to put in the subject field of the certificate.
* @param publicKey The PublicKey to be wrapped in the certificate * @param publicKey The PublicKey to be wrapped in the certificate.
* @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate * @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate.
* @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes * @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes.
* @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes * @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes.
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided. * @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
* @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS. * @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS.
* This certificate is not marked as a CA cert to be similar in nature to commercial certificates. * This certificate is not marked as a CA cert to be similar in nature to commercial certificates.
*/ */
@JvmStatic @JvmStatic
fun createServerCert(subject: X500Name, publicKey: PublicKey, fun createTlsServerCert(subject: X500Name, publicKey: PublicKey,
ca: CertificateAndKey, ca: CertificateAndKeyPair,
subjectAlternativeNameDomains: List<String>, subjectAlternativeNameDomains: List<String>,
subjectAlternativeNameIps: List<String>, subjectAlternativeNameIps: List<String>,
signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509Certificate {
validityWindow: Pair<Int, Int> = DEFAULT_VALIDITY_WINDOW): X509Certificate {
val issuer = X509CertificateHolder(ca.certificate.encoded).subject val issuer = X509CertificateHolder(ca.certificate.encoded).subject
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter) val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate)
val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) } val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) }
val ipAddresses = subjectAlternativeNameIps.filter { val ipAddresses = subjectAlternativeNameIps.filter {
IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it) IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it)
}.map { GeneralName(GeneralName.iPAddress, it) } }.map { GeneralName(GeneralName.iPAddress, it) }
return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, signatureScheme, window, subjectAlternativeName = dnsNames + ipAddresses) return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses)
} }
/** /**
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection * Build a certificate path from a trusted root certificate to a target certificate. This will always return a path
* @param x509Certificate certificate to save * directly from the root to the target, with no intermediate certificates (presuming that path is valid).
* @param filename Target filename *
* @param rootCertAndKey trusted root certificate that will be the start of the path.
* @param targetCertAndKey certificate the path ends at.
* @param revocationEnabled whether revocation of certificates in the path should be checked.
*/
fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair,
targetCertAndKey: X509Certificate,
revocationEnabled: Boolean): CertPathBuilderResult {
val intermediateCertificates = setOf(targetCertAndKey)
val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates))
val certPathFactory = CertPathBuilder.getInstance("PKIX")
val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null)
val certPathParameters = try {
PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply {
certificate = targetCertAndKey
})
} catch (ex: InvalidAlgorithmParameterException) {
throw RuntimeException(ex)
}.apply {
addCertStore(certStore)
isRevocationEnabled = revocationEnabled
}
return certPathFactory.build(certPathParameters)
}
/**
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection.
* @param x509Certificate certificate to save.
* @param filename Target filename.
*/ */
@JvmStatic @JvmStatic
fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) { fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) {
@ -172,9 +222,9 @@ object X509Utilities {
} }
/** /**
* Helper method to load back a .pem/.cer format file copy of a certificate * Helper method to load back a .pem/.cer format file copy of a certificate.
* @param filename Source filename * @param filename Source filename.
* @return The X509Certificate that was encoded in the file * @return The X509Certificate that was encoded in the file.
*/ */
@JvmStatic @JvmStatic
fun loadCertificateFromPEMFile(filename: Path): X509Certificate { fun loadCertificateFromPEMFile(filename: Path): X509Certificate {
@ -186,14 +236,14 @@ object X509Utilities {
} }
/** /**
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine * An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine.
* @param keyStoreFilePath KeyStore path to save output to * @param keyStoreFilePath KeyStore path to save output to.
* @param storePassword access password for KeyStore * @param storePassword access password for KeyStore.
* @param keyPassword PrivateKey access password for the generated keys. * @param keyPassword PrivateKey access password for the generated keys.
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same. * It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore * @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore.
* @param caKeyPassword password to unlock private keys in the CA KeyStore * @param caKeyPassword password to unlock private keys in the CA KeyStore.
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications * @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications.
*/ */
fun createKeystoreForSSL(keyStoreFilePath: Path, fun createKeystoreForSSL(keyStoreFilePath: Path,
storePassword: String, storePassword: String,
@ -203,12 +253,12 @@ object X509Utilities {
commonName: X500Name, commonName: X500Name,
signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore { signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore {
val rootCA = caKeyStore.getCertificateAndKey(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword) val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword)
val intermediateCA = caKeyStore.getCertificateAndKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword) val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword)
val serverKey = generateKeyPair(signatureScheme) val serverKey = generateKeyPair(signatureScheme)
val host = InetAddress.getLocalHost() val host = InetAddress.getLocalHost()
val serverCert = createServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress), signatureScheme) val serverCert = createTlsServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress))
val keyPass = keyPassword.toCharArray() val keyPass = keyPassword.toCharArray()
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword) val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword)
@ -228,7 +278,7 @@ object X509Utilities {
/** /**
* Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an * Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an
* exception * exception.
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName } fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName }
@ -238,7 +288,7 @@ fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName
* adds one. * adds one.
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { attr -> commonName } fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName }
/** /**
* Rebuild the distinguished name, replacing the common name with a value generated from the provided function. * Rebuild the distinguished name, replacing the common name with a value generated from the provided function.
@ -267,6 +317,7 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N
} }
val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString() val X500Name.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.location: String get() = getRDNs(BCStyle.L).first().first.value.toString()
class CertificateStream(val input: InputStream) { class CertificateStream(val input: InputStream) {
@ -275,4 +326,4 @@ class CertificateStream(val input: InputStream) {
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
} }
data class CertificateAndKey(val certificate: X509Certificate, val keyPair: KeyPair) data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair)

View File

@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable
* the flow to run at the scheduled time. * the flow to run at the scheduled time.
*/ */
interface FlowLogicRefFactory { interface FlowLogicRefFactory {
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
} }
@CordaSerializable @CordaSerializable
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg") class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException(
"${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
/** /**
* A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox. * A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox.

View File

@ -0,0 +1,14 @@
package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Any [FlowLogic] which is schedulable and is designed to be invoked by a [net.corda.core.contracts.SchedulableState]
* must have this annotation. If it's missing [FlowLogicRefFactory.create] will throw an exception when it comes time
* to schedule the next activity in [net.corda.core.contracts.SchedulableState.nextScheduledActivity].
*/
@Target(CLASS)
@Inherited
@MustBeDocumented
annotation class SchedulableFlow

View File

@ -0,0 +1,15 @@
package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Any [FlowLogic] which is to be started by the RPC interface ([net.corda.core.messaging.CordaRPCOps.startFlowDynamic]
* and [net.corda.core.messaging.CordaRPCOps.startTrackedFlowDynamic]) must have this annotation. If it's missing the
* flow will not be allowed to start and an exception will be thrown.
*/
@Target(CLASS)
@Inherited
@MustBeDocumented
// TODO Consider a different name, something along the lines of SchedulableFlow
annotation class StartableByRPC

View File

@ -16,7 +16,6 @@ abstract class AbstractParty(val owningKey: PublicKey) {
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
override fun hashCode(): Int = owningKey.hashCode() override fun hashCode(): Int = owningKey.hashCode()
abstract fun toAnonymous(): AnonymousParty
abstract fun nameOrNull(): X500Name? abstract fun nameOrNull(): X500Name?
abstract fun ref(bytes: OpaqueBytes): PartyAndReference abstract fun ref(bytes: OpaqueBytes): PartyAndReference

View File

@ -18,5 +18,4 @@ class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
override fun nameOrNull(): X500Name? = null override fun nameOrNull(): X500Name? = null
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
override fun toAnonymous() = this
} }

View File

@ -1,6 +1,7 @@
package net.corda.core.identity package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -25,11 +26,10 @@ import java.security.PublicKey
* *
* @see CompositeKey * @see CompositeKey
*/ */
// TODO: Remove "open" from [Party] once deprecated crypto.Party class is removed class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
open class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) { constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public)
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey) override fun toString() = name.toString()
override fun toString() = "${owningKey.toBase58String()} ($name)"
override fun nameOrNull(): X500Name? = name override fun nameOrNull(): X500Name? = name
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes) override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
} }

View File

@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps {
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>>
/** /**
* Start the given flow with the given arguments. * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T> fun <T : Any> startFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowHandle<T>
/** /**
* Start the given flow with the given arguments, returning an [Observable] with a single observation of the * Start the given flow with the given arguments, returning an [Observable] with a single observation of the
* result of running the flow. * result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T> fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>

View File

@ -8,28 +8,24 @@ import java.util.function.Function
* Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file * Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file
* to extend a Corda node with additional application services. * to extend a Corda node with additional application services.
*/ */
abstract class CordaPluginRegistry( abstract class CordaPluginRegistry {
/** /**
* List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should * 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. * potentially be able to live in a process separate from the node itself.
*/ */
open val webApis: List<Function<CordaRPCOps, out Any>> = emptyList(), open val webApis: List<Function<CordaRPCOps, out Any>> get() = emptyList()
/** /**
* Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*. * 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 * 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("<folder-in-jar>").toExternalForm() * be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm()
*/ */
open val staticServeDirs: Map<String, String> = emptyMap(), open val staticServeDirs: Map<String, String> get() = emptyMap()
/** @Suppress("unused")
* A Map with an entry for each consumed Flow used by the webAPIs. @Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " +
* The key of each map entry should contain the FlowLogic<T> class name. "@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR)
* The associated map values are the union of all concrete class names passed to the Flow constructor. open val requiredFlows: Map<String, Set<String>> get() = emptyMap()
* Standard java.lang.* and kotlin.* types do not need to be included explicitly.
* This is used to extend the white listed Flows that can be initiated from the ServiceHub invokeFlowAsync method.
*/
open val requiredFlows: Map<String, Set<String>> = emptyMap(),
/** /**
* List of lambdas constructing additional long lived services to be hosted within the node. * List of lambdas constructing additional long lived services to be hosted within the node.
@ -37,13 +33,13 @@ abstract class CordaPluginRegistry(
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will * 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. * allow access to the Flow factory and Flow initiation entry points there.
*/ */
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList() open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
) {
/** /**
* Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized. * Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized.
* *
* For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by * For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that
* adding the @CordaSerializable annotation or via this method. * either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method.
** **
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future. * @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
*/ */

View File

@ -1,10 +1,11 @@
package net.corda.core.node package net.corda.core.node
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.keys import net.corda.core.crypto.DigitalSignature
import net.corda.core.node.services.* import net.corda.core.node.services.*
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import java.security.KeyPair import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
import java.time.Clock import java.time.Clock
/** /**
@ -82,23 +83,107 @@ interface ServiceHub : ServicesForResolution {
} }
/** /**
* Helper property to shorten code for fetching the Node's KeyPair associated with the * Helper property to shorten code for fetching the the [PublicKey] portion of the
* public legalIdentity Party from the key management service. * Node's primary signing identity.
* Typical use is during signing in flows and for unit test signing. * Typical use is during signing in flows and for unit test signing.
* * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
* TODO: legalIdentity can now be composed of multiple keys, should we return a list of keyPairs here? Right now * the matching [PrivateKey] will be looked up internally and used to sign.
* the logic assumes the legal identity has a composite key with only one node * If the key is actually a CompositeKey, the first leaf key hosted on this node
* will be used to create the signature.
*/ */
val legalIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.legalIdentity.owningKey.keys) val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentity.owningKey
/** /**
* Helper property to shorten code for fetching the Node's KeyPair associated with the * Helper property to shorten code for fetching the the [PublicKey] portion of the
* public notaryIdentity Party from the key management service. It is assumed that this is only * Node's Notary signing identity. It is required that the Node hosts a notary service,
* used in contexts where the Node knows it is hosting a Notary Service. Otherwise, it will throw * otherwise an IllegalArgumentException will be thrown.
* an IllegalArgumentException.
* Typical use is during signing in flows and for unit test signing. * Typical use is during signing in flows and for unit test signing.
* * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
* TODO: same problem as with legalIdentityKey. * the matching [PrivateKey] will be looked up internally and used to sign.
* If the key is actually a [CompositeKey], the first leaf key hosted on this node
* will be used to create the signature.
*/ */
val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey.keys) val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey
/**
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
* using keys stored inside the node.
* @param builder The [TransactionBuilder] to seal with the node's signature.
* Any existing signatures on the builder will be preserved.
* @param publicKey The [PublicKey] matched to the internal [PrivateKey] to use in signing this transaction.
* If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node
* to sign with.
* @return Returns a SignedTransaction with the new node signature attached.
*/
fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey): SignedTransaction {
val sig = keyManagementService.sign(builder.toWireTransaction().id.bytes, publicKey)
builder.addSignatureUnchecked(sig)
return builder.toSignedTransaction(false)
}
/**
* Helper method to construct an initial partially signed transaction from a TransactionBuilder
* using the default identity key contained in the node.
* @param builder The TransactionBuilder to seal with the node's signature.
* Any existing signatures on the builder will be preserved.
* @return Returns a SignedTransaction with the new node signature attached.
*/
fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey)
/**
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
* using a set of keys all held in this node.
* @param builder The [TransactionBuilder] to seal with the node's signature.
* Any existing signatures on the builder will be preserved.
* @param signingPubKeys A list of [PublicKeys] used to lookup the matching [PrivateKey] and sign.
* @throws IllegalArgumentException is thrown if any keys are unavailable locally.
* @return Returns a [SignedTransaction] with the new node signature attached.
*/
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: List<PublicKey>): SignedTransaction {
var stx: SignedTransaction? = null
for (pubKey in signingPubKeys) {
stx = if (stx == null) {
signInitialTransaction(builder, pubKey)
} else {
addSignature(stx, pubKey)
}
}
return stx!!
}
/**
* Helper method to create an additional signature for an existing (partially) [SignedTransaction].
* @param signedTransaction The [SignedTransaction] to which the signature will apply.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing.
* @return The [DigitalSignature.WithKey] generated by signing with the internally held [PrivateKey].
*/
fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey = keyManagementService.sign(signedTransaction.id.bytes, publicKey)
/**
* Helper method to create an additional signature for an existing (partially) SignedTransaction
* using the default identity signing key of the node.
* @param signedTransaction The SignedTransaction to which the signature will apply.
* @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey.
*/
fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey = createSignature(signedTransaction, legalIdentityKey)
/**
* Helper method to append an additional signature to an existing (partially) [SignedTransaction].
* @param signedTransaction The [SignedTransaction] to which the signature will be added.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing.
* @return A new [SignedTransaction] with the addition of the new signature.
*/
fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction = signedTransaction + createSignature(signedTransaction, publicKey)
/**
* Helper method to ap-pend an additional signature for an existing (partially) [SignedTransaction]
* using the default identity signing key of the node.
* @param signedTransaction The [SignedTransaction] to which the signature will be added.
* @return A new [SignedTransaction] with the addition of the new signature.
*/
fun addSignature(signedTransaction: SignedTransaction): SignedTransaction = addSignature(signedTransaction, legalIdentityKey)
} }

View File

@ -1,10 +1,13 @@
package net.corda.core.node.services package net.corda.core.node.services
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
/** /**
* An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports * An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports
@ -14,6 +17,29 @@ import java.security.PublicKey
interface IdentityService { interface IdentityService {
fun registerIdentity(party: Party) fun registerIdentity(party: Party)
/**
* Verify and then store the certificates proving that an anonymous party's key is owned by the given full
* party.
*
* @param trustedRoot trusted root certificate, typically the R3 master signing certificate.
* @param anonymousParty an anonymised party belonging to the legal entity.
* @param path certificate path from the trusted root to the anonymised party.
* @throws IllegalArgumentException if the chain does not link the two parties, or if there is already an existing
* certificate chain for the anonymous party. Anonymous parties must always resolve to a single owning party.
*/
// TODO: Move this into internal identity service once available
@Throws(IllegalArgumentException::class)
fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath)
/**
* Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with
* the anonymous party and resolving it back to the given full party.
*
* @throws IllegalStateException if the anonymous party is not owned by the full party.
*/
@Throws(IllegalStateException::class)
fun assertOwnership(party: Party, anonymousParty: AnonymousParty)
/** /**
* Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be * Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be
* used in preference where possible. * used in preference where possible.
@ -29,6 +55,13 @@ interface IdentityService {
fun partyFromName(name: String): Party? fun partyFromName(name: String): Party?
fun partyFromX500Name(principal: X500Name): Party? fun partyFromX500Name(principal: X500Name): Party?
fun partyFromAnonymous(party: AnonymousParty): Party? fun partyFromAnonymous(party: AbstractParty): Party?
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party) fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
/**
* Get the certificate chain showing an anonymous party is owned by the given party.
*/
fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath?
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
} }

View File

@ -3,7 +3,8 @@ package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -18,8 +19,6 @@ import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import rx.Observable import rx.Observable
import java.io.InputStream import java.io.InputStream
import java.security.KeyPair
import java.security.PrivateKey
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -286,7 +285,7 @@ interface VaultService {
@Suspendable @Suspendable
fun generateSpend(tx: TransactionBuilder, fun generateSpend(tx: TransactionBuilder,
amount: Amount<Currency>, amount: Amount<Currency>,
to: PublicKey, to: AbstractParty,
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>> onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>>
// DOCSTART VaultStatesQuery // DOCSTART VaultStatesQuery
@ -371,32 +370,32 @@ class StatesNotAvailableException(override val message: String?, override val ca
/** /**
* The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example, * The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example,
* call out to a hardware security module that enforces various auditing and frequency-of-use requirements. * call out to a hardware security module that enforces various auditing and frequency-of-use requirements.
*
* The current interface is obviously not usable for those use cases: this is just where we'd put a real signing
* interface if/when one is developed.
*/ */
interface KeyManagementService { interface KeyManagementService {
/** Returns a snapshot of the current pubkey->privkey mapping. */ /**
val keys: Map<PublicKey, PrivateKey> * Returns a snapshot of the current signing [PublicKey]s.
* For each of these keys a [PrivateKey] is available, that can be used later for signing.
*/
val keys: Set<PublicKey>
@Throws(IllegalStateException::class) /**
fun toPrivate(publicKey: PublicKey) = keys[publicKey] ?: throw IllegalStateException("No private key known for requested public key ${publicKey.toStringShort()}") * Generates a new random [KeyPair] and adds it to the internal key storage. Returns the public part of the pair.
*/
@Suspendable
fun freshKey(): PublicKey
@Throws(IllegalArgumentException::class) /** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data.
fun toKeyPair(publicKey: PublicKey): KeyPair { * @param bytes The data to sign over using the chosen key.
when (publicKey) { * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
is CompositeKey -> throw IllegalArgumentException("Got CompositeKey when single PublicKey expected.") * or previously generated via the [freshKey] method.
else -> return KeyPair(publicKey, toPrivate(publicKey)) * If the [PublicKey] is actually a [CompositeKey] the first leaf signing key hosted by the node is used.
} * @throws IllegalArgumentException if the input key is not a member of [keys].
} * TODO A full [KeyManagementService] implementation needs to record activity to the [AuditService] and to limit signing to
* appropriately authorised contexts and initiating users.
/** Returns the first [KeyPair] matching any of the [publicKeys] */ */
@Throws(IllegalArgumentException::class) @Suspendable
fun toKeyPair(publicKeys: Iterable<PublicKey>) = publicKeys.first { keys.contains(it) }.let { toKeyPair(it) } fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey
/** Generates a new random key and adds it to the exposed map. */
fun freshKey(): KeyPair
} }
// TODO: Move to a more appropriate location // TODO: Move to a more appropriate location

View File

@ -37,7 +37,7 @@ interface QueryableState : ContractState {
* @param version The version number of this instance within the family. * @param version The version number of this instance within the family.
* @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema. * @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema.
*/ */
abstract class MappedSchema(schemaFamily: Class<*>, open class MappedSchema(schemaFamily: Class<*>,
val version: Int, val version: Int,
val mappedTypes: Iterable<Class<*>>) { val mappedTypes: Iterable<Class<*>>) {
val name: String = schemaFamily.name val name: String = schemaFamily.name

View File

@ -26,9 +26,12 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.Logger import org.slf4j.Logger
import sun.security.provider.certpath.X509CertPath
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.* import java.util.*
object DefaultKryoCustomizer { object DefaultKryoCustomizer {
@ -97,6 +100,12 @@ object DefaultKryoCustomizer {
// Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...). // Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...).
addDefaultSerializer(InputStream::class.java, InputStreamSerializer) addDefaultSerializer(InputStream::class.java, InputStreamSerializer)
register(CertPath::class.java, CertPathSerializer)
register(X509CertPath::class.java, CertPathSerializer)
// TODO: We shouldn't need to serialize raw certificates, and if we do then we need a cleaner solution
// than this mess.
val x509CertObjectClazz = Class.forName("org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject")
register(x509CertObjectClazz, X509CertificateSerializer)
register(X500Name::class.java, X500NameSerializer) register(X500Name::class.java, X500NameSerializer)
register(BCECPrivateKey::class.java, PrivateKeySerializer) register(BCECPrivateKey::class.java, PrivateKeySerializer)

View File

@ -31,6 +31,9 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.security.PrivateKey import java.security.PrivateKey
import java.security.PublicKey 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.security.spec.InvalidKeySpecException
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -617,6 +620,36 @@ object X500NameSerializer : Serializer<X500Name>() {
} }
} }
/**
* For serialising an [CertPath] in an X.500 standard format.
*/
@ThreadSafe
object CertPathSerializer : Serializer<CertPath>() {
val factory = CertificateFactory.getInstance("X.509")
override fun read(kryo: Kryo, input: Input, type: Class<CertPath>): CertPath {
return factory.generateCertPath(input)
}
override fun write(kryo: Kryo, output: Output, obj: CertPath) {
output.writeBytes(obj.encoded)
}
}
/**
* For serialising an [CX509Certificate] in an X.500 standard format.
*/
@ThreadSafe
object X509CertificateSerializer : Serializer<X509Certificate>() {
val factory = CertificateFactory.getInstance("X.509")
override fun read(kryo: Kryo, input: Input, type: Class<X509Certificate>): X509Certificate {
return factory.generateCertificate(input) as X509Certificate
}
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
output.writeBytes(obj.encoded)
}
}
class KryoPoolWithContext(val baseKryoPool: KryoPool, val contextKey: Any, val context: Any) : KryoPool { class KryoPoolWithContext(val baseKryoPool: KryoPool, val contextKey: Any, val context: Any) : KryoPool {
override fun <T : Any?> run(callback: KryoCallback<T>): T { override fun <T : Any?> run(callback: KryoCallback<T>): T {
val kryo = borrow() val kryo = borrow()

View File

@ -1,13 +1,16 @@
package net.corda.core.transactions package net.corda.core.transactions
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.MerkleTreeException
import net.corda.core.crypto.PartialMerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.p2PKryo import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import java.security.PublicKey
import net.corda.core.serialization.withoutReferences import net.corda.core.serialization.withoutReferences
import java.security.PublicKey
fun <T : Any> serializedHash(x: T): SecureHash { fun <T : Any> serializedHash(x: T): SecureHash {
return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } } return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } }
@ -91,7 +94,7 @@ class FilteredLeaves(
*/ */
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean { fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
val checkList = availableComponents.map { checkingFun(it) } val checkList = availableComponents.map { checkingFun(it) }
return (!checkList.isEmpty()) && checkList.all { true } return (!checkList.isEmpty()) && checkList.all { it }
} }
} }

View File

@ -3,14 +3,12 @@ package net.corda.core.transactions
import net.corda.core.contracts.AttachmentResolutionException import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.node.ServiceHub
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.sign import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.* import java.util.*
@ -146,14 +144,5 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class) @Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class)
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services) fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services)
/**
* Utility to simplify the act of signing the transaction.
*
* @param keyPair the signer's public/private key pair.
*
* @return a digital signature of the transaction.
*/
fun signWithECDSA(keyPair: KeyPair) = keyPair.sign(this.id.bytes)
override fun toString(): String = "${javaClass.simpleName}(id=$id)" override fun toString(): String = "${javaClass.simpleName}(id=$id)"
} }

View File

@ -59,8 +59,10 @@ class LazyPool<A>(
* the returned iterable will be inaccurate. * the returned iterable will be inaccurate.
*/ */
fun close(): Iterable<A> { fun close(): Iterable<A> {
lifeCycle.transition(State.STARTED, State.FINISHED) lifeCycle.justTransition(State.FINISHED)
return poolQueue val elements = poolQueue.toList()
poolQueue.clear()
return elements
} }
inline fun <R> run(withInstance: (A) -> R): R { inline fun <R> run(withInstance: (A) -> R): R {

View File

@ -24,12 +24,14 @@ fun validateLegalName(normalizedLegalName: String) {
rules.forEach { it.validate(normalizedLegalName) } rules.forEach { it.validate(normalizedLegalName) }
} }
val WHITESPACE = "\\s++".toRegex()
/** /**
* The normalize function will trim the input string, replace any multiple spaces with a single space, * The normalize function will trim the input string, replace any multiple spaces with a single space,
* and normalize the string according to NFKC normalization form. * and normalize the string according to NFKC normalization form.
*/ */
fun normaliseLegalName(legalName: String): String { fun normaliseLegalName(legalName: String): String {
val trimmedLegalName = legalName.trim().replace(Regex("\\s+"), " ") val trimmedLegalName = legalName.trim().replace(WHITESPACE, " ")
return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC) return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC)
} }

View File

@ -13,7 +13,7 @@ class LifeCycle<S : Enum<S>>(initial: S) {
private val lock = ReentrantReadWriteLock() private val lock = ReentrantReadWriteLock()
private var state = initial private var state = initial
/** Assert that the lifecycle in the [requiredState] */ /** Assert that the lifecycle in the [requiredState]. */
fun requireState(requiredState: S) { fun requireState(requiredState: S) {
requireState({ "Required state to be $requiredState, was $it" }) { it == requiredState } requireState({ "Required state to be $requiredState, was $it" }) { it == requiredState }
} }
@ -28,11 +28,18 @@ class LifeCycle<S : Enum<S>>(initial: S) {
} }
} }
/** Transition the state from [from] to [to] */ /** Transition the state from [from] to [to]. */
fun transition(from: S, to: S) { fun transition(from: S, to: S) {
lock.writeLock().withLock { lock.writeLock().withLock {
require(state == from) { "Required state to be $from to transition to $to, was $state" } require(state == from) { "Required state to be $from to transition to $to, was $state" }
state = to state = to
} }
} }
/** Transition the state to [to] without performing a current state check. */
fun justTransition(to: S) {
lock.writeLock().withLock {
state = to
}
}
} }

View File

@ -4,9 +4,11 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.crypto.* import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -59,13 +61,13 @@ abstract class AbstractStateReplacementFlow {
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val myKey = serviceHub.myInfo.legalIdentity.owningKey val myKey = serviceHub.myInfo.legalIdentity
val me = listOf(myKey) val me = listOf(myKey)
val signatures = if (participants == me) { val signatures = if (participants == me) {
getNotarySignatures(stx) getNotarySignatures(stx)
} else { } else {
collectSignatures(participants - me, stx) collectSignatures((participants - me).map { it.owningKey }, stx)
} }
val finalTx = stx + signatures val finalTx = stx + signatures
@ -73,7 +75,7 @@ abstract class AbstractStateReplacementFlow {
return finalTx.tx.outRef(0) return finalTx.tx.outRef(0)
} }
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>>
@Suspendable @Suspendable
private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> { private fun collectSignatures(participants: Iterable<PublicKey>, stx: SignedTransaction): List<DigitalSignature.WithKey> {
@ -187,8 +189,7 @@ abstract class AbstractStateReplacementFlow {
} }
private fun sign(stx: SignedTransaction): DigitalSignature.WithKey { private fun sign(stx: SignedTransaction): DigitalSignature.WithKey {
val myKey = serviceHub.legalIdentityKey return serviceHub.createSignature(stx)
return myKey.sign(stx.id)
} }
} }
} }

View File

@ -0,0 +1,259 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.toBase58String
import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import java.security.PublicKey
/**
* The [CollectSignaturesFlow] is used to automate the collection of counter-party signatures for a given transaction.
*
* You would typically use this flow after you have built a transaction with the TransactionBuilder and signed it with
* your key pair. If there are additional signatures to collect then they can be collected using this flow. Signatures
* are collected based upon the [WireTransaction.mustSign] property which contains the union of all the PublicKeys
* listed in the transaction's commands as well as a notary's public key, if required. This flow returns a
* [SignedTransaction] which can then be passed to the [FinalityFlow] for notarisation. The other side of this flow is
* the [SignTransactionFlow].
*
* **WARNING**: This flow ONLY works with [ServiceHub.legalIdentityKey]s and WILL break if used with randomly generated
* keys by the [ServiceHub.keyManagementService].
*
* Usage:
*
* - Call the [CollectSignaturesFlow] flow as a [subFlow] and pass it a [SignedTransaction] which has at least been
* signed by the transaction creator (and possibly an oracle, if required)
* - The flow expects that the calling node has signed the provided transaction, if not the flow will fail
* - The flow will also fail if:
* 1. The provided transaction is invalid
* 2. Any of the required signing parties cannot be found in the [ServiceHub.networkMapCache] of the initiator
* 3. If the wrong key has been used by a counterparty to sign the transaction
* 4. The counterparty rejects the provided transaction
* - The flow will return a [SignedTransaction] with all the counter-party signatures (but not the notary's!)
* - If the provided transaction has already been signed by all counter-parties then this flow simply returns the
* provided transaction without contacting any counter-parties
* - Call the [FinalityFlow] with the return value of this flow
*
* Example - issuing a multi-lateral agreement which requires N signatures:
*
* val builder = TransactionType.General.Builder(notaryRef)
* val issueCommand = Command(Agreement.Commands.Issue(), state.participants)
*
* builder.withItems(state, issueCommand)
* builder.toWireTransaction().toLedgerTransaction(serviceHub).verify()
*
* // Transaction creator signs transaction.
* val ptx = builder.signWith(serviceHub.legalIdentityKey).toSignedTransaction(false)
*
* // Call to CollectSignaturesFlow.
* // The returned signed transaction will have all signatures appended apart from the notary's.
* val stx = subFlow(CollectSignaturesFlow(ptx))
*
* @param partiallySignedTx Transaction to collect the remaining signatures for
*/
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
// TODO: TwoPartyTradeFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that works is complete.
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
override val progressTracker: ProgressTracker = tracker()): FlowLogic<SignedTransaction>() {
companion object {
object COLLECTING : ProgressTracker.Step("Collecting signatures from counter-parties.")
object VERIFYING : ProgressTracker.Step("Verifying collected signatures.")
fun tracker() = ProgressTracker(COLLECTING, VERIFYING)
// TODO: Make the progress tracker adapt to the number of counter-parties to collect from.
}
@Suspendable override fun call(): SignedTransaction {
// TODO: Revisit when key management is properly fleshed out.
// This will break if a party uses anything other than their legalIdentityKey.
// Check the signatures which have already been provided and that the transaction is valid.
// Usually just the Initiator and possibly an oracle would have signed at this point.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
val signed = partiallySignedTx.sigs.map { it.by }
val notSigned = partiallySignedTx.tx.mustSign - signed
// One of the signatures collected so far MUST be from the initiator of this flow.
require(partiallySignedTx.sigs.any { it.by == myKey }) {
"The Initiator of CollectSignaturesFlow must have signed the transaction."
}
// The signatures must be valid and the transaction must be valid.
partiallySignedTx.verifySignatures(*notSigned.toTypedArray())
partiallySignedTx.tx.toLedgerTransaction(serviceHub).verify()
// Determine who still needs to sign.
progressTracker.currentStep = COLLECTING
val notaryKey = partiallySignedTx.tx.notary?.owningKey
// If present, we need to exclude the notary's PublicKey as the notary signature is collected separately with
// the FinalityFlow.
val unsigned = if (notaryKey != null) notSigned - notaryKey else notSigned
// If the unsigned counter-parties list is empty then we don't need to collect any more signatures here.
if (unsigned.isEmpty()) return partiallySignedTx
// Collect signatures from all counter-parties and append them to the partially signed transaction.
val counterpartySignatures = keysToParties(unsigned).map { collectSignature(it) }
val stx = partiallySignedTx + counterpartySignatures
// Verify all but the notary's signature if the transaction requires a notary, otherwise verify all signatures.
progressTracker.currentStep = VERIFYING
if (notaryKey != null) stx.verifySignatures(notaryKey) else stx.verifySignatures()
return stx
}
/**
* Lookup the [Party] object for each [PublicKey] using the [ServiceHub.networkMapCache].
*/
@Suspendable private fun keysToParties(keys: List<PublicKey>): List<Party> = keys.map {
// TODO: Revisit when IdentityService supports resolution of a (possibly random) public key to a legal identity key.
val partyNode = serviceHub.networkMapCache.getNodeByLegalIdentityKey(it)
?: throw IllegalStateException("Party ${it.toBase58String()} not found on the network.")
partyNode.legalIdentity
}
/**
* Get and check the required signature.
*/
@Suspendable private fun collectSignature(counterparty: Party): DigitalSignature.WithKey {
return sendAndReceive<DigitalSignature.WithKey>(counterparty, partiallySignedTx).unwrap {
require(counterparty.owningKey.isFulfilledBy(it.by)) { "Not signed by the required Party." }
it
}
}
}
/**
* The [SignTransactionFlow] should be called in response to the [CollectSignaturesFlow]. It automates the signing of
* a transaction providing the transaction:
*
* 1. Should actually be signed by the [Party] invoking this flow
* 2. Is valid as per the contracts referenced in the transaction
* 3. Has been, at least, signed by the counter-party which created it
* 4. Conforms to custom checking provided in the [checkTransaction] method of the [SignTransactionFlow]
*
* Usage:
*
* - Subclass [SignTransactionFlow] - this can be done inside an existing flow (as shown below)
* - Override the [checkTransaction] method to add some custom verification logic
* - Call the flow via [FlowLogic.subFlow]
* - The flow returns the fully signed transaction once it has been committed to the ledger
*
* Example - checking and signing a transaction involving a [net.corda.core.contracts.DummyContract], see
* CollectSignaturesFlowTests.kt for further examples:
*
* class Responder(val otherParty: Party): FlowLogic<SignedTransaction>() {
* @Suspendable override fun call(): SignedTransaction {
* // [SignTransactionFlow] sub-classed as a singleton object.
* val flow = object : SignTransactionFlow(otherParty) {
* @Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
* val tx = stx.tx
* val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
* "Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337)
* }
* }
*
* // Invoke the subFlow, in response to the counterparty calling [CollectSignaturesFlow].
* val stx = subFlow(flow)
*
* return waitForLedgerCommit(stx.id)
* }
* }
*
* @param otherParty The counter-party which is providing you a transaction to sign.
*/
abstract class SignTransactionFlow(val otherParty: Party,
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<SignedTransaction>() {
companion object {
object RECEIVING : ProgressTracker.Step("Receiving transaction proposal for signing.")
object VERIFYING : ProgressTracker.Step("Verifying transaction proposal.")
object SIGNING : ProgressTracker.Step("Signing transaction proposal.")
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING)
}
@Suspendable override fun call(): SignedTransaction {
progressTracker.currentStep = RECEIVING
val checkedProposal = receive<SignedTransaction>(otherParty).unwrap { proposal ->
progressTracker.currentStep = VERIFYING
// Check that the Responder actually needs to sign.
checkMySignatureRequired(proposal)
// Check the signatures which have already been provided. Usually the Initiators and possibly an Oracle's.
checkSignatures(proposal)
// Resolve dependencies and verify, pass in the WireTransaction as we don't have all signatures.
subFlow(ResolveTransactionsFlow(proposal.tx, otherParty))
proposal.tx.toLedgerTransaction(serviceHub).verify()
// Perform some custom verification over the transaction.
try {
checkTransaction(proposal)
} catch(e: Exception) {
if (e is IllegalStateException || e is IllegalArgumentException || e is AssertionError)
throw FlowException(e)
else
throw e
}
// All good. Unwrap the proposal.
proposal
}
// Sign and send back our signature to the Initiator.
progressTracker.currentStep = SIGNING
val mySignature = serviceHub.createSignature(checkedProposal)
send(otherParty, mySignature)
// Return the fully signed transaction once it has been committed.
return waitForLedgerCommit(checkedProposal.id)
}
@Suspendable private fun checkSignatures(stx: SignedTransaction) {
require(stx.sigs.any { it.by == otherParty.owningKey }) {
"The Initiator of CollectSignaturesFlow must have signed the transaction."
}
val signed = stx.sigs.map { it.by }
val allSigners = stx.tx.mustSign
val notSigned = allSigners - signed
stx.verifySignatures(*notSigned.toTypedArray())
}
/**
* The [checkTransaction] method allows the caller of this flow to provide some additional checks over the proposed
* transaction received from the counter-party. For example:
*
* - Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the
* expected type and number of inputs and outputs
* - Checking that the properties of the outputs are as you would expect. Linking into any reference data sources
* might be appropriate here
* - Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as
* potentially the transaction creator has access to some of your state references
*
* **WARNING**: If appropriate checks, such as the ones listed above, are not defined then it is likely that your
* node will sign any transaction if it conforms to the contract code in the transaction's referenced contracts.
*
* [IllegalArgumentException], [IllegalStateException] and [AssertionError] will be caught and rethrown as flow
* exceptions i.e. the other side will be given information about what exact check failed.
*
* @param stx a partially signed transaction received from your counter-party.
* @throws FlowException if the proposed transaction fails the checks.
*/
@Suspendable abstract protected fun checkTransaction(stx: SignedTransaction)
@Suspendable private fun checkMySignatureRequired(stx: SignedTransaction) {
// TODO: Revisit when key management is properly fleshed out.
val myKey = serviceHub.myInfo.legalIdentity.owningKey
require(myKey in stx.tx.mustSign) {
"Party is not a participant for any of the input states of transaction ${stx.id}"
}
}
}

View File

@ -2,6 +2,8 @@ package net.corda.flows
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AbstractParty
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
@ -15,6 +17,7 @@ import java.security.PublicKey
* use the new updated state for future transactions. * use the new updated state for future transactions.
*/ */
@InitiatingFlow @InitiatingFlow
@StartableByRPC
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>( class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
originalState: StateAndRef<OldState>, originalState: StateAndRef<OldState>,
newContractClass: Class<out UpgradedContract<OldState, NewState>> newContractClass: Class<out UpgradedContract<OldState, NewState>>
@ -30,12 +33,12 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
@JvmStatic @JvmStatic
fun verify(input: ContractState, output: ContractState, commandData: Command) { fun verify(input: ContractState, output: ContractState, commandData: Command) {
val command = commandData.value as UpgradeCommand val command = commandData.value as UpgradeCommand
val participants: Set<PublicKey> = input.participants.toSet() val participantKeys: Set<PublicKey> = input.participants.map { it.owningKey }.toSet()
val keysThatSigned: Set<PublicKey> = commandData.signers.toSet() val keysThatSigned: Set<PublicKey> = commandData.signers.toSet()
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *> val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
requireThat { requireThat {
"The signing keys include all participant keys" using keysThatSigned.containsAll(participants) "The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract) "Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass) "Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input)) "Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
@ -51,14 +54,13 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
.withItems( .withItems(
stateRef, stateRef,
contractUpgrade.upgrade(stateRef.state.data), contractUpgrade.upgrade(stateRef.state.data),
Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants)) Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants.map { it.owningKey }))
} }
} }
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> { override fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> {
val stx = assembleBareTx(originalState, modification) val baseTx = assembleBareTx(originalState, modification)
.signWith(serviceHub.legalIdentityKey) val stx = serviceHub.signInitialTransaction(baseTx)
.toSignedTransaction(false)
return stx to originalState.state.data.participants return stx to originalState.state.data.participants
} }
} }

View File

@ -13,7 +13,7 @@ import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
/** /**
* Verifies the given transactions, then sends them to the named notaries. If the notary agrees that the transactions * Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
* are acceptable then they are from that point onwards committed to the ledger, and will be written through to the * are acceptable then they are from that point onwards committed to the ledger, and will be written through to the
* vault. Additionally they will be distributed to the parties reflected in the participants list of the states. * vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
* *
@ -37,6 +37,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() { override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker()) constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker()) constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
companion object { companion object {
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") { object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
@ -105,7 +106,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
// Calculate who is meant to see the results based on the participants involved. // Calculate who is meant to see the results based on the participants involved.
val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants } val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants }
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail? // TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail?
val parties = keys.mapNotNull { serviceHub.identityService.partyFromKey(it) }.toSet() val parties = keys.mapNotNull { serviceHub.identityService.partyFromAnonymous(it) }.toSet()
Pair(stx, parties) Pair(stx, parties)
} }
} }

View File

@ -2,6 +2,7 @@ package net.corda.flows
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
@ -24,11 +25,11 @@ class NotaryChangeFlow<out T : ContractState>(
progressTracker: ProgressTracker = tracker()) progressTracker: ProgressTracker = tracker())
: AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) { : AbstractStateReplacementFlow.Instigator<T, T, Party>(originalState, newNotary, progressTracker) {
override fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>> { override fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> {
val state = originalState.state val state = originalState.state
val tx = TransactionType.NotaryChange.Builder(originalState.state.notary) val tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
val participants: Iterable<PublicKey> val participants: Iterable<AbstractParty>
if (state.encumbrance == null) { if (state.encumbrance == null) {
val modifiedState = TransactionState(state.data, modification) val modifiedState = TransactionState(state.data, modification)
@ -39,10 +40,7 @@ class NotaryChangeFlow<out T : ContractState>(
participants = resolveEncumbrances(tx) participants = resolveEncumbrances(tx)
} }
val myKey = serviceHub.legalIdentityKey val stx = serviceHub.signInitialTransaction(tx)
tx.signWith(myKey)
val stx = tx.toSignedTransaction(false)
return Pair(stx, participants) return Pair(stx, participants)
} }
@ -53,14 +51,14 @@ class NotaryChangeFlow<out T : ContractState>(
* *
* @return union of all added states' participants * @return union of all added states' participants
*/ */
private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<PublicKey> { private fun resolveEncumbrances(tx: TransactionBuilder): Iterable<AbstractParty> {
val stateRef = originalState.ref val stateRef = originalState.ref
val txId = stateRef.txhash val txId = stateRef.txhash
val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId) val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId)
?: throw StateReplacementException("Transaction $txId not found") ?: throw StateReplacementException("Transaction $txId not found")
val outputs = issuingTx.tx.outputs val outputs = issuingTx.tx.outputs
val participants = mutableSetOf<PublicKey>() val participants = mutableSetOf<AbstractParty>()
var nextStateIndex = stateRef.index var nextStateIndex = stateRef.index
var newOutputPosition = tx.outputStates().size var newOutputPosition = tx.outputStates().size

View File

@ -3,7 +3,10 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.contracts.Timestamp import net.corda.core.contracts.Timestamp
import net.corda.core.crypto.* import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.crypto.keys
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
@ -144,8 +147,7 @@ object NotaryFlow {
} }
private fun sign(bits: ByteArray): DigitalSignature.WithKey { private fun sign(bits: ByteArray): DigitalSignature.WithKey {
val mySigningKey = serviceHub.notaryIdentityKey return serviceHub.keyManagementService.sign(bits, serviceHub.notaryIdentityKey)
return mySigningKey.sign(bits)
} }
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException { private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {

View File

@ -2,21 +2,21 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.DealState import net.corda.core.contracts.DealState
import net.corda.core.crypto.* import net.corda.core.contracts.requireThat
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.expandedCompositeKeys
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.trace import net.corda.core.utilities.trace
import net.corda.core.utilities.unwrap import net.corda.core.utilities.unwrap
import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
/** /**
@ -26,7 +26,7 @@ import java.security.PublicKey
* *
* TODO: Also, the term Deal is used here where we might prefer Agreement. * TODO: Also, the term Deal is used here where we might prefer Agreement.
* *
* TODO: Consider whether we can merge this with [TwoPartyTradeFlow] * TODO: Make this flow more generic.
* *
*/ */
object TwoPartyDealFlow { object TwoPartyDealFlow {
@ -34,151 +34,57 @@ object TwoPartyDealFlow {
@CordaSerializable @CordaSerializable
data class Handshake<out T>(val payload: T, val publicKey: PublicKey) data class Handshake<out T>(val payload: T, val publicKey: PublicKey)
@CordaSerializable
class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List<DigitalSignature.WithKey>)
/** /**
* Abstracted bilateral deal flow participant that initiates communication/handshake. * Abstracted bilateral deal flow participant that initiates communication/handshake.
*
* There's a good chance we can push at least some of this logic down into core flow logic
* and helper methods etc.
*/ */
abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic<SignedTransaction>() { abstract class Primary(override val progressTracker: ProgressTracker = Primary.tracker()) : FlowLogic<SignedTransaction>() {
companion object { companion object {
object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal") object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.")
object VERIFYING : ProgressTracker.Step("Verifying proposed transaction") fun tracker() = ProgressTracker(SENDING_PROPOSAL)
object SIGNING : ProgressTracker.Step("Signing transaction")
object NOTARY : ProgressTracker.Step("Getting notary signature")
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party")
object RECORDING : ProgressTracker.Step("Recording completed transaction")
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator")
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR)
} }
abstract val payload: Any abstract val payload: Any
abstract val notaryNode: NodeInfo abstract val notaryNode: NodeInfo
abstract val otherParty: Party abstract val otherParty: Party
abstract val myKeyPair: KeyPair abstract val myKey: PublicKey
@Suspendable
fun getPartialTransaction(): UntrustworthyData<SignedTransaction> {
progressTracker.currentStep = AWAITING_PROPOSAL
@Suspendable override fun call(): SignedTransaction {
progressTracker.currentStep = SENDING_PROPOSAL
// Make the first message we'll send to kick off the flow. // Make the first message we'll send to kick off the flow.
val hello = Handshake(payload, myKeyPair.public) val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey)
val maybeSTX = sendAndReceive<SignedTransaction>(otherParty, hello) // Wait for the FinalityFlow to finish on the other side and return the tx when it's available.
send(otherParty, hello)
return maybeSTX val signTransactionFlow = object : SignTransactionFlow(otherParty) {
override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx)
} }
@Suspendable subFlow(signTransactionFlow)
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
progressTracker.currentStep = VERIFYING
untrustedPartialTX.unwrap { stx -> val txHash = receive<SecureHash>(otherParty).unwrap { it }
progressTracker.nextStep()
// Check that the tx proposed by the buyer is valid. return waitForLedgerCommit(txHash)
val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.notaryIdentity.owningKey)
logger.trace { "Received partially signed transaction: ${stx.id}" }
checkDependencies(stx)
// This verifies that the transaction is contract-valid, even though it is missing signatures.
wtx.toLedgerTransaction(serviceHub).verify()
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
//
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
// we're reusing keys! So don't reuse keys!
// - This tx may include output states that impose odd conditions on the movement of the cash,
// once we implement state pairing.
//
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
// express flow state machines on top of the messaging layer.
return stx
}
} }
@Suspendable @Suspendable abstract fun checkProposal(stx: SignedTransaction)
private fun checkDependencies(stx: SignedTransaction) {
// Download and check all the transactions that this transaction depends on, but do not check this
// transaction itself.
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
subFlow(ResolveTransactionsFlow(dependencyTxIDs, otherParty))
} }
@Suspendable
override fun call(): SignedTransaction {
val stx: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
val ourSignature = computeOurSignature(stx)
val allPartySignedTx = stx + ourSignature
val notarySignatures = getNotarySignatures(allPartySignedTx)
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures)
progressTracker.currentStep = RECORDING
serviceHub.recordTransactions(fullySigned)
logger.trace { "Deal stored" }
progressTracker.currentStep = COPYING_TO_REGULATOR
val regulators = serviceHub.networkMapCache.regulatorNodes
if (regulators.isNotEmpty()) {
// If there are regulators in the network, then we could copy them in on the transaction via a sub-flow
// which would simply send them the transaction.
}
return fullySigned
}
@Suspendable
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
progressTracker.currentStep = NOTARY
return subFlow(NotaryFlow.Client(stx))
}
open fun computeOurSignature(partialTX: SignedTransaction): DigitalSignature.WithKey {
progressTracker.currentStep = SIGNING
return myKeyPair.sign(partialTX.id)
}
@Suspendable
private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey,
notarySignatures: List<DigitalSignature.WithKey>): SignedTransaction {
progressTracker.currentStep = SENDING_SIGS
val fullySigned = allPartySignedTx + notarySignatures
logger.trace { "Built finished transaction, sending back to other party!" }
send(otherParty, SignaturesFromPrimary(ourSignature, notarySignatures))
return fullySigned
}
}
/** /**
* Abstracted bilateral deal flow participant that is recipient of initial communication. * Abstracted bilateral deal flow participant that is recipient of initial communication.
*
* There's a good chance we can push at least some of this logic down into core flow logic
* and helper methods etc.
*/ */
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() { abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
companion object { companion object {
object RECEIVING : ProgressTracker.Step("Waiting for deal info") object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
object VERIFYING : ProgressTracker.Step("Verifying deal info") object VERIFYING : ProgressTracker.Step("Verifying deal info.")
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal") object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.")
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party") object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.")
object RECORDING : ProgressTracker.Step("Recording completed transaction") object RECORDING : ProgressTracker.Step("Recording completed transaction.")
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator.")
object COPYING_TO_COUNTERPARTY : ProgressTracker.Step("Copying counterparty.")
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING) fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING, COPYING_TO_REGULATOR, COPYING_TO_COUNTERPARTY)
} }
abstract val otherParty: Party abstract val otherParty: Party
@ -188,23 +94,35 @@ object TwoPartyDealFlow {
val handshake = receiveAndValidateHandshake() val handshake = receiveAndValidateHandshake()
progressTracker.currentStep = SIGNING progressTracker.currentStep = SIGNING
val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake) val (utx, additionalSigningPubKeys) = assembleSharedTX(handshake)
val stx = signWithOurKeys(additionalSigningPubKeys, ptx) val ptx = signWithOurKeys(additionalSigningPubKeys, utx)
val signatures = swapSignaturesWithPrimary(stx) logger.trace { "Signed proposed transaction." }
progressTracker.currentStep = COLLECTING_SIGNATURES
val stx = subFlow(CollectSignaturesFlow(ptx))
logger.trace { "Got signatures from other party, verifying ... " } logger.trace { "Got signatures from other party, verifying ... " }
val fullySigned = stx + signatures.sellerSig + signatures.notarySigs
fullySigned.verifySignatures()
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
progressTracker.currentStep = RECORDING progressTracker.currentStep = RECORDING
serviceHub.recordTransactions(fullySigned) val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single()
logger.trace { "Deal transaction stored" } logger.trace { "Recorded transaction." }
return fullySigned
progressTracker.currentStep = COPYING_TO_REGULATOR
val regulators = serviceHub.networkMapCache.regulatorNodes
if (regulators.isNotEmpty()) {
// Copy the transaction to every regulator in the network. This is obviously completely bogus, it's
// just for demo purposes.
regulators.forEach { send(it.serviceIdentities(ServiceType.regulator).first(), ftx) }
}
progressTracker.currentStep = COPYING_TO_COUNTERPARTY
// Send the final transaction hash back to the other party.
// We need this so we don't break the IRS demo and the SIMM Demo.
send(otherParty, ftx.id)
return ftx
} }
@Suspendable @Suspendable
@ -217,24 +135,9 @@ object TwoPartyDealFlow {
return handshake.unwrap { validateHandshake(it) } return handshake.unwrap { validateHandshake(it) }
} }
@Suspendable
private fun swapSignaturesWithPrimary(stx: SignedTransaction): SignaturesFromPrimary {
progressTracker.currentStep = SWAPPING_SIGNATURES
logger.trace { "Sending partially signed transaction to other party" }
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
return sendAndReceive<SignaturesFromPrimary>(otherParty, stx).unwrap { it }
}
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction { private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
// Now sign the transaction with whatever keys we need to move the cash. // Now sign the transaction with whatever keys we need to move the cash.
for (publicKey in signingPubKeys.expandedCompositeKeys) { return serviceHub.signInitialTransaction(ptx, signingPubKeys)
val privateKey = serviceHub.keyManagementService.toPrivate(publicKey)
ptx.signWith(KeyPair(publicKey, privateKey))
}
return ptx.toSignedTransaction(checkSufficientSignatures = false)
} }
@Suspendable protected abstract fun validateHandshake(handshake: Handshake<U>): Handshake<U> @Suspendable protected abstract fun validateHandshake(handshake: Handshake<U>): Handshake<U>
@ -244,17 +147,20 @@ object TwoPartyDealFlow {
@CordaSerializable @CordaSerializable
data class AutoOffer(val notary: Party, val dealBeingOffered: DealState) data class AutoOffer(val notary: Party, val dealBeingOffered: DealState)
/** /**
* One side of the flow for inserting a pre-agreed deal. * One side of the flow for inserting a pre-agreed deal.
*/ */
open class Instigator(override val otherParty: Party, open class Instigator(override val otherParty: Party,
override val payload: AutoOffer, override val payload: AutoOffer,
override val myKeyPair: KeyPair, override val myKey: PublicKey,
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() { override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() {
override val notaryNode: NodeInfo get() = override val notaryNode: NodeInfo get() =
serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single() serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single()
@Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat {
// Add some constraints here.
}
} }
/** /**
@ -281,5 +187,4 @@ object TwoPartyDealFlow {
return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey)) return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey))
} }
} }
} }

View File

@ -29,7 +29,7 @@ object TxKeyFlowUtilities {
*/ */
@Suspendable @Suspendable
fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey { fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey {
val key = flow.serviceHub.keyManagementService.freshKey().public val key = flow.serviceHub.keyManagementService.freshKey()
// TODO: Generate and sign certificate for the key, once we have signing support for composite keys // TODO: Generate and sign certificate for the key, once we have signing support for composite keys
// (in this case the legal identity key) // (in this case the legal identity key)
flow.send(otherSide, ProvidedTransactionKey(key, null)) flow.send(otherSide, ProvidedTransactionKey(key, null))

View File

@ -1,6 +1,7 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.testing.ALICE_PUBKEY import net.corda.testing.ALICE_PUBKEY
import org.junit.Test import org.junit.Test
@ -14,7 +15,7 @@ class DummyContractV2Tests {
@Test @Test
fun `upgrade from v1`() { fun `upgrade from v1`() {
val contractUpgrade = DummyContractV2() val contractUpgrade = DummyContractV2()
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
val v1Ref = StateRef(SecureHash.randomSHA256(), 0) val v1Ref = StateRef(SecureHash.randomSHA256(), 0)
val v1StateAndRef = StateAndRef(v1State, v1Ref) val v1StateAndRef = StateAndRef(v1State, v1Ref)
val (tx, _) = DummyContractV2().generateUpgradeFromV1(v1StateAndRef) val (tx, _) = DummyContractV2().generateUpgradeFromV1(v1StateAndRef)

View File

@ -2,13 +2,12 @@ package net.corda.core.contracts
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.utilities.DUMMY_PUBKEY_1 import net.corda.core.identity.AbstractParty
import net.corda.core.utilities.DUMMY_PUBKEY_2
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.corda.testing.MINI_CORP
import net.corda.testing.ledger import net.corda.testing.ledger
import net.corda.testing.transaction import net.corda.testing.transaction
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import java.time.Instant import java.time.Instant
import java.time.temporal.ChronoUnit import java.time.temporal.ChronoUnit
@ -19,9 +18,9 @@ class TransactionEncumbranceTests {
val state = Cash.State( val state = Cash.State(
amount = 1000.DOLLARS `issued by` defaultIssuer, amount = 1000.DOLLARS `issued by` defaultIssuer,
owner = DUMMY_PUBKEY_1 owner = MEGA_CORP
) )
val stateWithNewOwner = state.copy(owner = DUMMY_PUBKEY_2) val stateWithNewOwner = state.copy(owner = MINI_CORP)
val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z") val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z")
val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS) val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS)
@ -40,7 +39,7 @@ class TransactionEncumbranceTests {
data class State( data class State(
val validFrom: Instant val validFrom: Instant
) : ContractState { ) : ContractState {
override val participants: List<PublicKey> = emptyList() override val participants: List<AbstractParty> = emptyList()
override val contract: Contract = TEST_TIMELOCK_ID override val contract: Contract = TEST_TIMELOCK_ID
} }
} }
@ -52,7 +51,7 @@ class TransactionEncumbranceTests {
input { state } input { state }
output(encumbrance = 1) { stateWithNewOwner } output(encumbrance = 1) { stateWithNewOwner }
output("5pm time-lock") { timeLock } output("5pm time-lock") { timeLock }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
verifies() verifies()
} }
} }
@ -70,7 +69,7 @@ class TransactionEncumbranceTests {
input("state encumbered by 5pm time-lock") input("state encumbered by 5pm time-lock")
input("5pm time-lock") input("5pm time-lock")
output { stateWithNewOwner } output { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
timestamp(FIVE_PM) timestamp(FIVE_PM)
verifies() verifies()
} }
@ -89,7 +88,7 @@ class TransactionEncumbranceTests {
input("state encumbered by 5pm time-lock") input("state encumbered by 5pm time-lock")
input("5pm time-lock") input("5pm time-lock")
output { state } output { state }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
timestamp(FOUR_PM) timestamp(FOUR_PM)
this `fails with` "the time specified in the time-lock has passed" this `fails with` "the time specified in the time-lock has passed"
} }
@ -106,7 +105,7 @@ class TransactionEncumbranceTests {
transaction { transaction {
input("state encumbered by 5pm time-lock") input("state encumbered by 5pm time-lock")
output { stateWithNewOwner } output { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
timestamp(FIVE_PM) timestamp(FIVE_PM)
this `fails with` "Missing required encumbrance 1 in INPUT" this `fails with` "Missing required encumbrance 1 in INPUT"
} }
@ -118,7 +117,7 @@ class TransactionEncumbranceTests {
transaction { transaction {
input { state } input { state }
output(encumbrance = 0) { stateWithNewOwner } output(encumbrance = 0) { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
this `fails with` "Missing required encumbrance 0 in OUTPUT" this `fails with` "Missing required encumbrance 0 in OUTPUT"
} }
} }
@ -129,7 +128,7 @@ class TransactionEncumbranceTests {
input { state } input { state }
output(encumbrance = 2) { stateWithNewOwner } output(encumbrance = 2) { stateWithNewOwner }
output { timeLock } output { timeLock }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
this `fails with` "Missing required encumbrance 2 in OUTPUT" this `fails with` "Missing required encumbrance 2 in OUTPUT"
} }
} }
@ -146,7 +145,7 @@ class TransactionEncumbranceTests {
input("state encumbered by some other state") input("state encumbered by some other state")
input("5pm time-lock") input("5pm time-lock")
output { stateWithNewOwner } output { stateWithNewOwner }
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() } command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
timestamp(FIVE_PM) timestamp(FIVE_PM)
this `fails with` "Missing required encumbrance 1 in INPUT" this `fails with` "Missing required encumbrance 1 in INPUT"
} }

View File

@ -11,7 +11,6 @@ import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.testing.ALICE_PUBKEY
import org.junit.Test import org.junit.Test
import java.security.KeyPair import java.security.KeyPair
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -95,7 +94,7 @@ class TransactionTests {
@Test @Test
fun `transactions with no inputs can have any notary`() { fun `transactions with no inputs can have any notary`() {
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
val inputs = emptyList<StateAndRef<*>>() val inputs = emptyList<StateAndRef<*>>()
val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB)) val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB))
val commands = emptyList<AuthenticatedObject<CommandData>>() val commands = emptyList<AuthenticatedObject<CommandData>>()
@ -120,7 +119,7 @@ class TransactionTests {
@Test @Test
fun `transaction verification fails for duplicate inputs`() { fun `transaction verification fails for duplicate inputs`() {
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY) val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
val stateRef = StateRef(SecureHash.randomSHA256(), 0) val stateRef = StateRef(SecureHash.randomSHA256(), 0)
val stateAndRef = StateAndRef(baseOutState, stateRef) val stateAndRef = StateAndRef(baseOutState, stateRef)
val inputs = listOf(stateAndRef, stateAndRef) val inputs = listOf(stateAndRef, stateAndRef)
@ -148,7 +147,7 @@ class TransactionTests {
@Test @Test
fun `general transactions cannot change notary`() { fun `general transactions cannot change notary`() {
val notary: Party = DUMMY_NOTARY val notary: Party = DUMMY_NOTARY
val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), notary) val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary)
val outState = inState.copy(notary = ALICE) val outState = inState.copy(notary = ALICE)
val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0))) val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0)))
val outputs = listOf(outState) val outputs = listOf(outState)

View File

@ -12,9 +12,7 @@ import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_PUBKEY_1 import net.corda.core.utilities.DUMMY_PUBKEY_1
import net.corda.core.utilities.TEST_TX_TIME import net.corda.core.utilities.TEST_TX_TIME
import net.corda.testing.MEGA_CORP import net.corda.testing.*
import net.corda.testing.MEGA_CORP_PUBKEY
import net.corda.testing.ledger
import org.junit.Test import org.junit.Test
import java.security.PublicKey import java.security.PublicKey
import kotlin.test.* import kotlin.test.*
@ -30,20 +28,20 @@ class PartialMerkleTreeTest {
output("MEGA_CORP cash") { output("MEGA_CORP cash") {
Cash.State( Cash.State(
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1), amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = MEGA_CORP_PUBKEY owner = MEGA_CORP
) )
} }
output("dummy cash 1") { output("dummy cash 1") {
Cash.State( Cash.State(
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1), amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
owner = DUMMY_PUBKEY_1 owner = MINI_CORP
) )
} }
} }
transaction { transaction {
input("MEGA_CORP cash") input("MEGA_CORP cash")
output("MEGA_CORP cash".output<Cash.State>().copy(owner = DUMMY_PUBKEY_1)) output("MEGA_CORP cash".output<Cash.State>().copy(owner = MINI_CORP))
command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() } command(MEGA_CORP_PUBKEY) { Cash.Commands.Move() }
timestamp(TEST_TX_TIME) timestamp(TEST_TX_TIME)
this.verifies() this.verifies()
@ -61,7 +59,7 @@ class PartialMerkleTreeTest {
@Test @Test
fun `building Merkle tree - no hashes`() { fun `building Merkle tree - no hashes`() {
assertFailsWith<MerkleTreeException> { MerkleTree.Companion.getMerkleTree(emptyList()) } assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList()) }
} }
@Test @Test
@ -98,7 +96,7 @@ class PartialMerkleTreeTest {
fun filtering(elem: Any): Boolean { fun filtering(elem: Any): Boolean {
return when (elem) { return when (elem) {
is StateRef -> true is StateRef -> true
is TransactionState<*> -> elem.data.participants[0].keys == DUMMY_PUBKEY_1.keys is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys
is Command -> MEGA_CORP_PUBKEY in elem.signers is Command -> MEGA_CORP_PUBKEY in elem.signers
is Timestamp -> true is Timestamp -> true
is PublicKey -> elem == MEGA_CORP_PUBKEY is PublicKey -> elem == MEGA_CORP_PUBKEY

View File

@ -2,7 +2,6 @@ package net.corda.core.crypto
import net.corda.core.div import net.corda.core.div
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.i2p.crypto.eddsa.EdDSAEngine
import net.corda.testing.getTestX509Name import net.corda.testing.getTestX509Name
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.GeneralName import org.bouncycastle.asn1.x509.GeneralName
@ -57,7 +56,7 @@ class X509UtilitiesTest {
val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert")) val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert"))
val subjectDN = getTestX509Name("Server Cert") val subjectDN = getTestX509Name("Server Cert")
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME) val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
val serverCert = X509Utilities.createServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54")) val serverCert = X509Utilities.createTlsServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54"))
assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name
assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert
serverCert.checkValidity(Date()) // throws on verification problems serverCert.checkValidity(Date()) // throws on verification problems
@ -107,7 +106,7 @@ class X509UtilitiesTest {
val tmpKeyStore = tempFile("keystore.jks") val tmpKeyStore = tempFile("keystore.jks")
val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test")) val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"))
val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512") val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
val edDSACert = X509Utilities.createServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54")) val edDSACert = X509Utilities.createTlsServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54"))
// Save the EdDSA private key with cert chains. // Save the EdDSA private key with cert chains.
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass") val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
@ -177,14 +176,14 @@ class X509UtilitiesTest {
// Load signing intermediate CA cert // Load signing intermediate CA cert
val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass") val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
val caCertAndKey = caKeyStore.getCertificateAndKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass") val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass")
// Generate server cert and private key and populate another keystore suitable for SSL // Generate server cert and private key and populate another keystore suitable for SSL
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name) X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
// Load back server certificate // Load back server certificate
val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass") val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
val serverCertAndKey = serverKeyStore.getCertificateAndKey(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass") val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass")
serverCertAndKey.certificate.checkValidity(Date()) serverCertAndKey.certificate.checkValidity(Date())
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey) serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
@ -349,4 +348,18 @@ class X509UtilitiesTest {
return keyStore return keyStore
} }
@Test
fun `Get correct private key type from Keystore`() {
val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair)
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword")
keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate))
val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray())
val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword")
assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key
assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey)
}
} }

View File

@ -0,0 +1,192 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.Command
import net.corda.core.contracts.DummyContract
import net.corda.core.contracts.TransactionType
import net.corda.core.contracts.requireThat
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.unwrap
import net.corda.flows.CollectSignaturesFlow
import net.corda.flows.FinalityFlow
import net.corda.flows.SignTransactionFlow
import net.corda.testing.MINI_CORP_KEY
import net.corda.testing.node.MockNetwork
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.util.concurrent.ExecutionException
import kotlin.test.assertFailsWith
class CollectSignaturesFlowTests {
lateinit var mockNet: MockNetwork
lateinit var a: MockNetwork.MockNode
lateinit var b: MockNetwork.MockNode
lateinit var c: MockNetwork.MockNode
lateinit var notary: Party
@Before
fun setup() {
mockNet = MockNetwork()
val nodes = mockNet.createSomeNodes(3)
a = nodes.partyNodes[0]
b = nodes.partyNodes[1]
c = nodes.partyNodes[2]
notary = nodes.notaryNode.info.notaryIdentity
mockNet.runNetwork()
CollectSigsTestCorDapp.registerFlows(a.services)
CollectSigsTestCorDapp.registerFlows(b.services)
CollectSigsTestCorDapp.registerFlows(c.services)
}
@After
fun tearDown() {
mockNet.stopNodes()
}
object CollectSigsTestCorDapp {
// Would normally be called by custom service init in a CorDapp.
fun registerFlows(pluginHub: PluginServiceHub) {
pluginHub.registerFlowInitiator(TestFlow.Initiator::class.java) { TestFlow.Responder(it) }
pluginHub.registerFlowInitiator(TestFlowTwo.Initiator::class.java) { TestFlowTwo.Responder(it) }
}
}
// With this flow, the initiators sends an "offer" to the responder, who then initiates the collect signatures flow.
// This flow is a more simplifed version of the "TwoPartyTrade" flow and is a useful example of how both the
// "collectSignaturesFlow" and "SignTransactionFlow" can be used in practise.
object TestFlow {
@InitiatingFlow
class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
send(otherParty, state)
val flow = object : SignTransactionFlow(otherParty) {
@Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
val tx = stx.tx
"There should only be one output state" using (tx.outputs.size == 1)
"There should only be one output state" using (tx.inputs.isEmpty())
val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
"Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337)
}
}
val stx = subFlow(flow)
val ftx = waitForLedgerCommit(stx.id)
return ftx
}
}
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val state = receive<DummyContract.MultiOwnerState>(otherParty).unwrap { it }
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
val builder = TransactionType.General.Builder(notary = notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx))
val ftx = subFlow(FinalityFlow(stx)).single()
return ftx
}
}
}
// With this flow, the initiator starts the "CollectTransactionFlow". It is then the responders responsibility to
// override "checkTransaction" and add whatever logic their require to verify the SignedTransaction they are
// receiving off the wire.
object TestFlowTwo {
@InitiatingFlow
class Initiator(val state: DummyContract.MultiOwnerState, val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable
override fun call(): SignedTransaction {
val notary = serviceHub.networkMapCache.notaryNodes.single().notaryIdentity
val command = Command(DummyContract.Commands.Create(), state.participants.map { it.owningKey })
val builder = TransactionType.General.Builder(notary = notary).withItems(state, command)
val ptx = serviceHub.signInitialTransaction(builder)
val stx = subFlow(CollectSignaturesFlow(ptx))
val ftx = subFlow(FinalityFlow(stx)).single()
return ftx
}
}
class Responder(val otherParty: Party) : FlowLogic<SignedTransaction>() {
@Suspendable override fun call(): SignedTransaction {
val flow = object : SignTransactionFlow(otherParty) {
@Suspendable override fun checkTransaction(stx: SignedTransaction) = requireThat {
val tx = stx.tx
"There should only be one output state" using (tx.outputs.size == 1)
"There should only be one output state" using (tx.inputs.isEmpty())
val magicNumberState = tx.outputs.single().data as DummyContract.MultiOwnerState
"Must be 1337 or greater" using (magicNumberState.magicNumber >= 1337)
}
}
val stx = subFlow(flow)
return waitForLedgerCommit(stx.id)
}
}
}
@Test
fun `successfully collects two signatures`() {
val magicNumber = 1337
val parties = listOf(a.info.legalIdentity, b.info.legalIdentity, c.info.legalIdentity)
val state = DummyContract.MultiOwnerState(magicNumber, parties)
val flow = a.services.startFlow(TestFlowTwo.Initiator(state, b.info.legalIdentity))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
result.verifySignatures()
println(result.tx)
println(result.sigs)
}
@Test
fun `no need to collect any signatures`() {
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1))
val ptx = a.services.signInitialTransaction(onePartyDummyContract)
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
result.verifySignatures()
println(result.tx)
println(result.sigs)
}
@Test
fun `fails when not signed by initiator`() {
val onePartyDummyContract = DummyContract.generateInitial(1337, notary, a.info.legalIdentity.ref(1))
val ptx = onePartyDummyContract.signWith(MINI_CORP_KEY).toSignedTransaction(false)
val flow = a.services.startFlow(CollectSignaturesFlow(ptx))
mockNet.runNetwork()
assertFailsWith<ExecutionException>("The Initiator of CollectSignaturesFlow must have signed the transaction.") {
flow.resultFuture.get()
}
}
@Test
fun `passes with multiple initial signatures`() {
val twoPartyDummyContract = DummyContract.generateInitial(1337, notary,
a.info.legalIdentity.ref(1),
b.info.legalIdentity.ref(2),
b.info.legalIdentity.ref(3))
val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
val signedByBoth = b.services.addSignature(signedByA)
val flow = a.services.startFlow(CollectSignaturesFlow(signedByBoth))
mockNet.runNetwork()
val result = flow.resultFuture.getOrThrow()
println(result.tx)
println(result.sigs)
}
}

View File

@ -1,14 +1,17 @@
package net.corda.core.flows package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.contracts.asset.Cash import net.corda.contracts.asset.Cash
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.services.unconsumedStates import net.corda.core.node.services.unconsumedStates
import net.corda.core.serialization.OpaqueBytes import net.corda.core.serialization.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.ContractUpgradeFlow import net.corda.flows.ContractUpgradeFlow
@ -25,9 +28,7 @@ import net.corda.testing.startRpcClient
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import java.util.* import java.util.*
import java.util.concurrent.ExecutionException
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFailsWith import kotlin.test.assertFailsWith
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -57,9 +58,8 @@ class ContractUpgradeFlowTest {
fun `2 parties contract upgrade`() { fun `2 parties contract upgrade`() {
// Create dummy contract. // Create dummy contract.
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1)) val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey) val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
.signWith(b.services.legalIdentityKey) val stx = b.services.addSignature(signedByA)
.toSignedTransaction()
a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity))) a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity)))
mockNet.runNetwork() mockNet.runNetwork()
@ -69,10 +69,10 @@ class ContractUpgradeFlowTest {
requireNotNull(atx) requireNotNull(atx)
requireNotNull(btx) requireNotNull(btx)
// The request is expected to be rejected because party B haven't authorise the upgrade yet. // The request is expected to be rejected because party B hasn't authorised the upgrade yet.
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(ExecutionException::class) { rejectedFuture.get() } assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade. // Party B authorise the contract state upgrade.
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java) b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
@ -81,7 +81,7 @@ class ContractUpgradeFlowTest {
val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture val resultFuture = a.services.startFlow(ContractUpgradeFlow(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
mockNet.runNetwork() mockNet.runNetwork()
val result = resultFuture.get() val result = resultFuture.getOrThrow()
fun check(node: MockNetwork.MockNode) { fun check(node: MockNetwork.MockNode) {
val nodeStx = node.database.transaction { val nodeStx = node.database.transaction {
@ -108,7 +108,7 @@ class ContractUpgradeFlowTest {
rpcAddress = startRpcServer( rpcAddress = startRpcServer(
rpcUser = user, rpcUser = user,
ops = CordaRPCOpsImpl(node.services, node.smm, node.database) ops = CordaRPCOpsImpl(node.services, node.smm, node.database)
).get().hostAndPort, ).get().broker.hostAndPort!!,
username = user.username, username = user.username,
password = user.password password = user.password
).get() ).get()
@ -119,17 +119,16 @@ class ContractUpgradeFlowTest {
rpcDriver { rpcDriver {
// Create dummy contract. // Create dummy contract.
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1)) val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey) val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
.signWith(b.services.legalIdentityKey) val stx = b.services.addSignature(signedByA)
.toSignedTransaction()
val user = rpcTestUser.copy(permissions = setOf( val user = rpcTestUser.copy(permissions = setOf(
startFlowPermission<FinalityFlow>(), startFlowPermission<FinalityInvoker>(),
startFlowPermission<ContractUpgradeFlow<*, *>>() startFlowPermission<ContractUpgradeFlow<*, *>>()
)) ))
val rpcA = startProxy(a, user) val rpcA = startProxy(a, user)
val rpcB = startProxy(b, user) val rpcB = startProxy(b, user)
val handle = rpcA.startFlow(::FinalityFlow, stx, setOf(a.info.legalIdentity, b.info.legalIdentity)) val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.legalIdentity, b.info.legalIdentity))
mockNet.runNetwork() mockNet.runNetwork()
handle.returnValue.getOrThrow() handle.returnValue.getOrThrow()
@ -143,7 +142,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
assertFailsWith(ExecutionException::class) { rejectedFuture.get() } assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
// Party B authorise the contract state upgrade. // Party B authorise the contract state upgrade.
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java) rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
@ -154,7 +153,7 @@ class ContractUpgradeFlowTest {
DummyContractV2::class.java).returnValue DummyContractV2::class.java).returnValue
mockNet.runNetwork() mockNet.runNetwork()
val result = resultFuture.get() val result = resultFuture.getOrThrow()
// Check results. // Check results.
listOf(a, b).forEach { listOf(a, b).forEach {
val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) } val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
@ -186,21 +185,21 @@ class ContractUpgradeFlowTest {
val firstState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() } val firstState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() }
assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.") assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.")
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.") assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
assertEquals(listOf(a.info.legalIdentity.owningKey), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.") assertEquals<Collection<AbstractParty>>(listOf(a.info.legalIdentity), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
} }
class CashV2 : UpgradedContract<Cash.State, CashV2.State> { class CashV2 : UpgradedContract<Cash.State, CashV2.State> {
override val legacyContract = Cash::class.java override val legacyContract = Cash::class.java
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<PublicKey>) : FungibleAsset<Currency> { data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
override val owner: PublicKey = owners.first() override val owner: AbstractParty = owners.first()
override val exitKeys = (owners + amount.token.issuer.party.owningKey).toSet() override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet()
override val contract = CashV2() override val contract = CashV2()
override val participants = owners override val participants = owners
override fun move(newAmount: Amount<Issued<Currency>>, newOwner: PublicKey) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner)) override fun move(newAmount: Amount<Issued<Currency>>, newOwner: AbstractParty) = copy(amount = amount.copy(newAmount.quantity), owners = listOf(newOwner))
override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)" override fun toString() = "${Emoji.bagOfCash}New Cash($amount at ${amount.token.issuer} owned by $owner)"
override fun withNewOwner(newOwner: PublicKey) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner))) override fun withNewOwner(newOwner: AbstractParty) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
} }
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner)) override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
@ -210,4 +209,11 @@ class ContractUpgradeFlowTest {
// Dummy Cash contract for testing. // Dummy Cash contract for testing.
override val legalContractReference = SecureHash.sha256("") override val legalContractReference = SecureHash.sha256("")
} }
@StartableByRPC
class FinalityInvoker(val transaction: SignedTransaction,
val extraRecipients: Set<Party>) : FlowLogic<List<SignedTransaction>>() {
@Suspendable
override fun call(): List<SignedTransaction> = subFlow(FinalityFlow(transaction, extraRecipients))
}
} }

View File

@ -12,7 +12,7 @@ import net.corda.flows.ResolveTransactionsFlow
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction
import net.corda.testing.MEGA_CORP import net.corda.testing.MEGA_CORP
import net.corda.testing.MEGA_CORP_KEY import net.corda.testing.MEGA_CORP_KEY
import net.corda.testing.MINI_CORP_PUBKEY import net.corda.testing.MINI_CORP
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
@ -94,7 +94,7 @@ class ResolveTransactionsFlowTest {
val count = 50 val count = 50
var cursor = stx2 var cursor = stx2
repeat(count) { repeat(count) {
val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP_PUBKEY) val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP)
.addSignatureUnchecked(NullSignature) .addSignatureUnchecked(NullSignature)
.toSignedTransaction(false) .toSignedTransaction(false)
a.database.transaction { a.database.transaction {
@ -113,13 +113,13 @@ class ResolveTransactionsFlowTest {
fun `triangle of transactions resolves fine`() { fun `triangle of transactions resolves fine`() {
val stx1 = makeTransactions().first val stx1 = makeTransactions().first
val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP_PUBKEY).run { val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run {
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
toSignedTransaction() toSignedTransaction()
} }
val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP_PUBKEY).run { val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run {
signWith(MEGA_CORP_KEY) signWith(MEGA_CORP_KEY)
signWith(DUMMY_NOTARY_KEY) signWith(DUMMY_NOTARY_KEY)
toSignedTransaction() toSignedTransaction()
@ -173,7 +173,7 @@ class ResolveTransactionsFlowTest {
it.signWith(DUMMY_NOTARY_KEY) it.signWith(DUMMY_NOTARY_KEY)
it.toSignedTransaction(false) it.toSignedTransaction(false)
} }
val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP_PUBKEY).let { val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let {
it.signWith(MEGA_CORP_KEY) it.signWith(MEGA_CORP_KEY)
it.signWith(DUMMY_NOTARY_KEY) it.signWith(DUMMY_NOTARY_KEY)
it.toSignedTransaction() it.toSignedTransaction()

View File

@ -4,7 +4,6 @@ import net.corda.core.identity.Party
import net.corda.core.utilities.ALICE import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB import net.corda.core.utilities.BOB
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.testing.MOCK_IDENTITY_SERVICE
import net.corda.testing.node.MockNetwork import net.corda.testing.node.MockNetwork
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -17,7 +16,6 @@ class TxKeyFlowUtilitiesTests {
@Before @Before
fun before() { fun before() {
net = MockNetwork(false) net = MockNetwork(false)
net.identities += MOCK_IDENTITY_SERVICE.identities
} }
@Test @Test

View File

@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.whenever import com.nhaarman.mockito_kotlin.whenever
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.services.AttachmentStorage import net.corda.core.node.services.AttachmentStorage
import net.corda.core.node.services.StorageService import net.corda.core.node.services.StorageService
@ -53,7 +54,7 @@ class AttachmentClassLoaderTests {
class AttachmentDummyContract : Contract { class AttachmentDummyContract : Contract {
data class State(val magicNumber: Int = 0) : ContractState { data class State(val magicNumber: Int = 0) : ContractState {
override val contract = ATTACHMENT_TEST_PROGRAM_ID override val contract = ATTACHMENT_TEST_PROGRAM_ID
override val participants: List<PublicKey> override val participants: List<AbstractParty>
get() = listOf() get() = listOf()
} }

View File

@ -2,10 +2,10 @@ package net.corda.core.node
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -20,7 +20,7 @@ class VaultUpdateTests {
} }
private class DummyState : ContractState { private class DummyState : ContractState {
override val participants: List<PublicKey> override val participants: List<AbstractParty>
get() = emptyList() get() = emptyList()
override val contract = VaultUpdateTests.DummyContract override val contract = VaultUpdateTests.DummyContract
} }

View File

@ -3,8 +3,11 @@ package net.corda.core.serialization
import com.esotericsoftware.kryo.Kryo import com.esotericsoftware.kryo.Kryo
import com.google.common.primitives.Ints import com.google.common.primitives.Ints
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.node.services.messaging.Ack import net.corda.node.services.messaging.Ack
import net.corda.node.services.persistence.NodeAttachmentService 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.assertThat
import org.assertj.core.api.Assertions.assertThatThrownBy import org.assertj.core.api.Assertions.assertThatThrownBy
import org.junit.Before import org.junit.Before
@ -12,6 +15,8 @@ import org.junit.Test
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -136,6 +141,24 @@ class KryoTests {
assertEquals(-1, readRubbishStream.read()) assertEquals(-1, readRubbishStream.read())
} }
@Test
fun `serialize - deserialize X509Certififcate`() {
val expected = X509Utilities.createSelfSignedCACert(ALICE.name).certificate
val serialized = expected.serialize(kryo).bytes
val actual: X509Certificate = serialized.deserialize(kryo)
assertEquals(expected, actual)
}
@Test
fun `serialize - deserialize X509CertPath`() {
val rootCA = X509Utilities.createSelfSignedCACert(ALICE.name)
val certificate = X509Utilities.createTlsServerCert(BOB.name, BOB_PUBKEY, rootCA, emptyList(), emptyList())
val expected = X509Utilities.createCertificatePath(rootCA, certificate, false).certPath
val serialized = expected.serialize(kryo).bytes
val actual: CertPath = serialized.deserialize(kryo)
assertEquals(expected, actual)
}
@CordaSerializable @CordaSerializable
private data class Person(val name: String, val birthday: Instant?) private data class Person(val name: String, val birthday: Instant?)

View File

@ -2,14 +2,19 @@ package net.corda.core.serialization
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.AbstractParty
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.* import net.corda.core.utilities.DUMMY_KEY_2
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.core.utilities.TEST_TX_TIME
import net.corda.testing.MEGA_CORP
import net.corda.testing.MEGA_CORP_KEY
import net.corda.testing.MINI_CORP import net.corda.testing.MINI_CORP
import net.corda.testing.generateStateRef import net.corda.testing.generateStateRef
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
import java.util.* import java.util.*
import kotlin.test.assertEquals import kotlin.test.assertEquals
@ -27,12 +32,12 @@ class TransactionSerializationTests {
data class State( data class State(
val deposit: PartyAndReference, val deposit: PartyAndReference,
val amount: Amount<Currency>, val amount: Amount<Currency>,
override val owner: PublicKey) : OwnableState { override val owner: AbstractParty) : OwnableState {
override val contract: Contract = TEST_PROGRAM_ID override val contract: Contract = TEST_PROGRAM_ID
override val participants: List<PublicKey> override val participants: List<AbstractParty>
get() = listOf(owner) get() = listOf(owner)
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner)) override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner))
} }
interface Commands : CommandData { interface Commands : CommandData {
@ -44,9 +49,9 @@ class TransactionSerializationTests {
// It refers to a fake TX/state that we don't bother creating here. // It refers to a fake TX/state that we don't bother creating here.
val depositRef = MINI_CORP.ref(1) val depositRef = MINI_CORP.ref(1)
val fakeStateRef = generateStateRef() val fakeStateRef = generateStateRef()
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef) val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), DUMMY_NOTARY), fakeStateRef)
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY) val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), DUMMY_NOTARY)
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, DUMMY_KEY_1.public), DUMMY_NOTARY) val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), DUMMY_NOTARY)
lateinit var tx: TransactionBuilder lateinit var tx: TransactionBuilder
@ -54,14 +59,14 @@ class TransactionSerializationTests {
@Before @Before
fun setup() { fun setup() {
tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems( tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(
inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(DUMMY_KEY_1.public)) inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(MEGA_CORP.owningKey))
) )
} }
@Test @Test
fun signWireTX() { fun signWireTX() {
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
tx.signWith(DUMMY_KEY_1) tx.signWith(MEGA_CORP_KEY)
val signedTX = tx.toSignedTransaction() val signedTX = tx.toSignedTransaction()
// Now check that the signature we just made verifies. // Now check that the signature we just made verifies.
@ -81,7 +86,7 @@ class TransactionSerializationTests {
tx.toSignedTransaction() tx.toSignedTransaction()
} }
tx.signWith(DUMMY_KEY_1) tx.signWith(MEGA_CORP_KEY)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val signedTX = tx.toSignedTransaction() val signedTX = tx.toSignedTransaction()
@ -104,7 +109,7 @@ class TransactionSerializationTests {
@Test @Test
fun timestamp() { fun timestamp() {
tx.setTime(TEST_TX_TIME, 30.seconds) tx.setTime(TEST_TX_TIME, 30.seconds)
tx.signWith(DUMMY_KEY_1) tx.signWith(MEGA_CORP_KEY)
tx.signWith(DUMMY_NOTARY_KEY) tx.signWith(DUMMY_NOTARY_KEY)
val stx = tx.toSignedTransaction() val stx = tx.toSignedTransaction()
assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint) assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint)

View File

@ -71,7 +71,7 @@ class SecureHashGenerator : Generator<SecureHash>(SecureHash::class.java) {
class StateRefGenerator : Generator<StateRef>(StateRef::class.java) { class StateRefGenerator : Generator<StateRef>(StateRef::class.java) {
override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef { override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef {
return StateRef(SecureHash.Companion.sha256(random.nextBytes(16)), random.nextInt(0, 10)) return StateRef(SecureHash.sha256(random.nextBytes(16)), random.nextInt(0, 10))
} }
} }

View File

@ -1,175 +1,205 @@
Working with the Corda Demo on Azure Marketplace Building a Corda Network on Azure Marketplace
================================================ =============================================
Corda ships with a VM image which can be used to deploy a pre-configured virtual machine on the `Microsoft Azure Marketplace <https://azure.microsoft.com/en-gb/overview/what-is-azure>`_ To help you design, build and test applications on Corda, called CorDapps, a Corda network can be deployed on the `Microsoft Azure Marketplace <https://azure.microsoft.com/en-gb/overview/what-is-azure>`_
This Corda network offering builds a pre-configured network of Corda nodes as Ubuntu virtual machines (VM). The network comprises of a Network Map Service node, a Notary node and up to nine Corda nodes using a version of Corda of your choosing. The following guide will also show you how to load a simple Yo! CorDapp which demonstrates the basic principles of Corda. When you are ready to go further with developing on Corda and start making contributions to the project head over to the `Corda.net <https://www.corda.net/>`_.
This Corda Demo VM is an easy option for running the demos; it is *NOT* a development environment. When you are ready to get developing on Corda and start making contributions to the project please clone the `GitHub Repos <https://github.com/corda/>`_ instead.
Pre-requisites Pre-requisites
-------------- --------------
* Ensure you have a registered Microsoft Azure account and are logged on to the Azure portal. * Ensure you have a registered Microsoft Azure account which can create virtual machines under your subscription(s) and you are logged on to the Azure portal (portal.azure.com)
* It is recommended you generate a private-public SSH key pair (see `here <https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys--2/>`_) * It is recommended you generate a private-public SSH key pair (see `here <https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys--2/>`_)
Deploying the VM Deploying the Corda Network
---------------- ---------------------------
Browse to portal.azure.com, login and search the Azure Marketplace for Corda and select 'Corda Single Ledger Network'.
Search the Azure Marketplace for Corda.
Click the 'Create' button. Click the 'Create' button.
STEP 1: Basics STEP 1: Basics
* **Name**: Choose an appropriate descriptive name for the VM Define the basic parameters which will be used to pre-configure your Corda nodes.
* **VM Disk Type**: Select 'SSD'
* **Username**: Your preferred user name for the administrator account when accessing via SSH
* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box below. Alternatively select 'Password' to use a password of your choice to administer the VM
* **Subscription**: Select your subscription name * **Resource prefix**: Choose an appropriate descriptive name for your Corda nodes. This name will prefix the node hostnames
* **Resource group**: Select 'Use existing'. From the drop-down menu, select your account group * **VM user name**: This is the user login name on the Ubuntu VMs. Leave it as azureuser or define your own
* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box. Alternatively select 'Password' to use a password of your choice to administer the VM
* **Restrict access by IP address**: Leave this as 'No' to allow access from any internet host, or provide an IP address or a range of IP addresses to limit access
* **Subscription**: Select which of your Azure subscriptions you want to use
* **Resource group**: Choose to 'Create new' and provide a useful name of your choice
* **Location**: Select the geographical location physically closest to you * **Location**: Select the geographical location physically closest to you
.. image:: resources/azure_vm_10_00_1.png .. image:: resources/azure_multi_node_step1.png
:width: 300px :width: 300px
Click 'OK' Click 'OK'
STEP 2: Size STEP 2: Network Size and Performance
A range of available hardware configurations will be presented, along with estimated costs. For the purposes of running the demos, a configuration of 2 cores and at least 14GB is recommended Define the number of Corda nodes in your network and the size of VM.
.. image:: resources/azure_vm_10_05_1.png * **Number of Network Map nodes**: There can only be one Network Map node in this network. Leave as '1'
* **Number of Notary nodes**: There can only be one Notary node in this network. Leave as '1'
* **Number of participant nodes**: This is the number of Corda nodes in your network. At least 2 nodes in your network is recommended (so you can send transactions between them). You can specific 1 participant node and use the Notary node as a second node. There is an upper limit of 9
* **Storage performance**: Leave as 'Standard'
* **Virtual machine size**: The size of the VM is automatically adjusted to suit the number of participant nodes selected. It is recommended to use the suggested values
.. image:: resources/azure_multi_node_step2.png
:width: 300px :width: 300px
Choose the required configuration and click 'Select'. Click 'OK'
STEP 3: Settings STEP 3: Corda Specific Options
Adjust any configuration settings required. For the purposes of running the Corda demos, all settings may be left as default. Define the version of Corda you want on your nodes and the type of notary.
.. image:: resources/azure_vm_10_16_1.png * **Corda version (as seen in Maven Central)**: Select the version of Corda you want your nodes to use from the drop down list. The version numbers can be seen in `Maven Central <http://repo1.maven.org/maven2/net/corda/corda/>`_, for example 0.11.0
* **Notary type**: Select either 'Non Validating' (notary only checks whether a state has been previously used and marked as historic) or 'Validating' (notary performs transaction verification by seeing input and output states, attachments and other transaction information). More information on notaries can be found `here <https://vimeo.com/album/4555732/video/214138458>`_
.. image:: resources/azure_multi_node_step3.png
:width: 300px :width: 300px
Click 'OK'
STEP 4: Summary STEP 4: Summary
The banner at the top of the dialog should read 'Validation passed' otherwise go back and adjust settings where needed. A summary of your selections is shown.
.. image:: resources/azure_vm_10_19.png .. image:: resources/azure_multi_node_step4.png
:width: 300px :width: 300px
Click 'OK' to proceed. Click 'OK' for your selection to be validated. If everything is ok you will see the message 'Validation passed'
Click 'OK'
STEP 5: Buy STEP 5: Buy
Click 'Purchase' to complete the configuration and start the VM deployment. Review the Azure Terms of Use and Privacy Policy and click 'Purchase' to buy the Azure VMs which will host your Corda nodes.
The VM will begin the deployment process, which typically takes 4-5 minutes to complete. To see progress, click on the "Deploying" icon displayed. The deployment process will start and typically takes 8-10 minutes to complete.
.. image:: resources/azure_vm_10_20.png Once deployed click 'Resources Groups', select the resource group you defined in Step 1 above and click 'Overview' to see the virtual machine details. The names of your VMs will be pre-fixed with the resource prefix value you defined in Step 1 above.
The Newtork Map Service node is suffixed nm0. The Notary node is suffixed not0. Your Corda participant nodes are suffixed node0, node1, node2 etc. Note down the **Public IP address** for your Corda nodes. You will need these to connect to UI screens via your web browser:
.. image:: resources/azure_ip.png
:width: 300px :width: 300px
Once deployed, click 'Overview' to see the virtual machine details. Note down the **Public IP address**. You will need this to connect to the demo screens via your web browser: Using the Yo! CorDapp
---------------------
Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages to other Corda nodes on the network. A Yo! message is a very simple transaction. The Yo! CorDapp demonstrates:
.. image:: resources/azure_vm_10_26.png - how transactions are only sent between Corda nodes which they are intended for and are not shared across the entire network by using the network map
:width: 300px - uses a pre-defined flow to orchestrate the ledger update automatically
- the contract imposes rules on the ledger updates
Viewing the SIMM Valuation demo * **Loading the Yo! CorDapp onto your nodes**
-------------------------------
The SIMM Valuation demo creates three nodes, representing three parties in the example workflow (Bank A, Bank B, Bank C). Each node listens on a different port - those used by the demo are:
**SIMM Valuation Demo ports:** **12005 (node A for Bank A)**, **12007 (node B for Bank B)**, **12009 (node C for Bank C)** The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory.
Open three browser tabs and direct each one to Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command:
For Corda nodes running release M10
.. sourcecode:: shell .. sourcecode:: shell
http://(public IP address):(port)/web/simmvaluationdemo cd /opt/corda/plugins
wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar
specifying each of the three ports above in different windows, e.g. For Corda nodes running release M11
.. sourcecode:: shell .. sourcecode:: shell
http://51.140.41.48/12005/web/simmvaluationdemo cd /opt/corda/plugins
wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar
You will be able to view the basic web interface identifying the different banks. Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal:
Now let's take a look at a transaction between Bank A and B which is not visible to Bank C. This illustrates the restricted data sharing feature of Corda, i.e. data is shared on a need-to-know basis. Nodes provide the dependency graph of a transaction they are sending to another node on demand, but there is no global broadcast of all transactions.
1. In the browser tab for Bank A (the top right hand corner shows which bank you are administering) click 'Create New Trade' from the top navigation bar
2. Select to trade with Bank B
3. Select 'EUR Fixed 1y EURIBOR 3m' from the drop down
4. Click 'Submit' to create the trade
5. In the browser tab for Bank B click 'View Portfolio' from the top navigation bar to see this new trade
6. In the browser tab for Bank C click 'View Portfolio' from the top navigation bar and you will not be able to see the trade, as expected
.. image:: resources/azure_vm_10_51.png
:width: 300px
.. note:: There is a known issue whereby some users may see a 400 error when navigating the SIMM Valuation demo. If you encounter this error, simply navigate back to the root page (http://*(public IP address)*:*(port)*/*web*/*simmvaluationdemo*) in the browser before continuing.
Viewing the IRS demo
--------------------
The IRS demo creates three nodes: Bank A, Bank B and a node that runs a notary, a network map and an interest rates oracle together. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the time on a simulated clock passes. Each bank node listens on a different port - those used by the demo are:
**IRS demo ports:** **11005 (node A for Bank A)**, **11007 (node B for Bank B)**
Open two browser tabs and direct one to each of the following:
.. sourcecode:: shell .. sourcecode:: shell
http://localhost:11005/web/irsdemo sudo systemctl restart corda
http://localhost:11007/web/irsdemo sudo systemctl restart corda-webserver
You will be able to see the nodes' view of the ledger. Repeat these steps on other Corda nodes on your network which you want to send or receive Yo messages.
.. image:: resources/azure_vm_10_52.png * **Verify the Yo! CorDapp is running**
Open a browser tab and browse to the following URL:
.. sourcecode:: shell
http://(public IP address):(port)/web/yo
where (public IP address) is the public IP address of one of your Corda nodes on the Azure Corda network and (port) is the web server port number for your Corda node, 10004 by default
You will now see the Yo! CordDapp web interface:
.. image:: resources/Yo_web_ui.png
:width: 300px :width: 300px
Now let's take a look at how the interest rates oracle provides interest rates for a deal with a semi-annual payment frequency, and how the two counterparties to the trade see the same deal information on their own nodes, i.e. you see what I see. * **Sending a Yo message via the web interface**
1. In the browser tab for Bank A click 'Create Deal' from the top navigation bar In the browser window type the following URL to send a Yo message to a target node on your Corda network:
2. Modify the terms of the IRS deal, or leave as default
3. Click 'Submit' to create the deal
4. In the browser tab for Bank A click 'Recent Deals' from the top navigation bar to view the deal
5. In the browser tab for Bank B click 'Recent Deals' from the top navigation bar to view the deal. Compare the economic details to those shown in the Bank A tab
.. image:: resources/azure_vm_10_54.png .. 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
http://40.69.40.42:10004/api/yo/yo?target=Corda 0.10.1 Node 1 in tstyo2
An easy way to see the Legal Names of Corda nodes on the network is to use the peers screen:
.. sourcecode:: shell
http://(public IP address):(port)/api/yo/peers
.. image:: resources/yo_peers2.png
:width: 300px :width: 300px
* **Viewing Yo messages**
Viewing logs (advanced users) To see Yo! messages sent to a particular node open a browser window and browse to the following URL:
-----------------------------
.. sourcecode:: shell
http://(public IP address):(port)/api/yo/yos
.. image:: resources/azure_yos.png
:width: 300px
Viewing logs
------------
Users may wish to view the raw logs generated by each node, which contain more information about the operations performed by each node. Users may wish to view the raw logs generated by each node, which contain more information about the operations performed by each node.
You can access these using an SSH client of your choice (e.g. Putty) and logging into the virtual machine using the public IP address. You can access these using an SSH client of your choice (e.g. Putty) and logging into the virtual machine using the public IP address.
Once logged in, navigate to Once logged in, navigate to the following directory for Corda logs (node-xxxxxx):
.. sourcecode:: shell .. sourcecode:: shell
/opt/simm-nodes/ /opt/corda/logs
for the SIMM Valuation demo logs and And navigate to the following directory for system logs (syslog):
.. sourcecode:: shell .. sourcecode:: shell
/opt/irs-nodes/ /var/log
for the IRS demo logs.
There are separate sub-directories for each of the three nodes (*nodea*, *nodeb*, *nodec*), each containing a */logs* sub-directory.
The name of the log file will follow the name given to the service it reflects, e.g. *node-clint-vm-test.log*.
.. image:: resources/azure_vm_10_47.png
:width: 300px
You can open log files with any text editor. You can open log files with any text editor.
.. image:: resources/azure_vm_10_49.png .. image:: resources/azure_vm_10_49.png
:width: 300px :width: 300px
.. image:: resources/azure_syslog.png
:width: 300px
Next Steps Next Steps
---------- ----------
Now you have taken a look at two Corda demos do go and visit the `dedicated Corda website <https://www.corda.net>`_ Now you have built a Corda network and used a basic Corda CorDapp do go and visit the `dedicated Corda website <https://www.corda.net>`_
Or to get straight into the Corda open source codebase, head over to the `Github Corda repo <https://www.github.com/corda>`_ Or to join the growing Corda community and get straight into the Corda open source codebase, head over to the `Github Corda repo <https://www.github.com/corda>`_

View File

@ -8,8 +8,11 @@ UNRELEASED
---------- ----------
* API changes: * API changes:
* Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with * ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
``InitiatingFlow``. ``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
* Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now
required to be annotated with ``@InitiatingFlow``.
* ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the * ``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 marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an
@ -29,25 +32,55 @@ UNRELEASED
* ``FlowLogic.getCounterpartyMarker`` is no longer used and been deprecated for removal. If you were using this to * ``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. manage multiple independent message streams with the same party in the same flow then use sub-flows instead.
* There are major changes to the ``Party`` class as part of confidential identities: * There are major changes to the ``Party`` class as part of confidential identities:
* ``Party`` has moved to the ``net.corda.core.identity`` package; there is a deprecated class in its place for * ``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 backwards compatibility, but it will be removed in a future release and developers should move to the new class as soon
as possible. as possible.
* There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. A new class * There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. This now replaces
``AnonymousParty`` has been added, which is intended to be used in place of ``Party`` or ``PublicKey`` in contract use of ``Party`` and ``PublicKey`` in state objects, and allows use of full or anonymised parties depending on
state objects. The exception to this is where the party in a contract state is intended to be well known, such as use-case.
issuer of a ``Cash`` state.
* Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the * 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. name. As a result all node legal names must now be structured as X.500 distinguished names.
* There are major changes to transaction signing in flows:
* You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most
of the details of this for you. They may get more complex in future as signing becomes a more featureful
operation.
* ``ServiceHub.legalIdentityKey`` no longer returns a ``KeyPair``, it instead returns just the ``PublicKey`` portion of this pair.
The ``ServiceHub.notaryIdentityKey`` has changed similarly. The goal of this change is to keep private keys
encapsulated and away from most flow code/Java code, so that the private key material can be stored in HSMs
and other key management devices.
* The ``KeyManagementService`` now provides no mechanism to request the node's ``PrivateKey`` objects directly.
Instead signature creation occurs in the ``KeyManagementService.sign``, with the ``PublicKey`` used to indicate
which of the node's multiple keys to use. This lookup also works for ``CompositeKey`` scenarios
and the service will search for a leaf key hosted on the node.
* The ``KeyManagementService.freshKey`` method now returns only the ``PublicKey`` portion of the newly generated ``KeyPair``
with the ``PrivateKey`` kept internally to the service.
* Flows which used to acquire a node's ``KeyPair``, typically via ``ServiceHub.legalIdentityKey``,
should instead use the helper methods on ``ServiceHub``. In particular to freeze a ``TransactionBuilder`` and
generate an initial partially signed ``SignedTransaction`` the flow should use ``ServiceHub.signInitialTransaction``.
Flows generating additional party signatures should use ``ServiceHub.createSignature``. Each of these methods is
provided with two signatures. One version that signs with the default node key, the other which allows key selection
by passing in the ``PublicKey`` partner of the desired signing key.
* The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but
should only be used as part of unit testing.
* The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version * 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 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 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 point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards
compatibility of CorDapp flows. compatibility of CorDapp flows.
Milestone 11.1
--------------
* Fix serialisation error when starting a flow.
* Automatically whitelist subclasses of `InputStream` when serialising.
* Fix exception in DemoBench on Windows when loading CorDapps into the Node Explorer.
* Detect when localhost resolution is broken on MacOSX, and provide instructions on how to fix it.
Milestone 11.0 Milestone 11.0
-------------- --------------

View File

@ -6,14 +6,15 @@ compatible language the easiest way to do so is using the client library. The li
node using a message queue protocol and then provides a simple RPC interface to interact with it. You make calls node using a message queue protocol and then provides a simple RPC interface to interact with it. You make calls
on a Java object as normal, and the marshalling back and forth is handled for you. on a Java object as normal, and the marshalling back and forth is handled for you.
The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``proxy`` method that The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``start`` method that
returns an implementation of the `CordaRPCOps`_ interface. A timeout parameter can be specified, and observables that returns a `CordaRPCConnection`_, holding an implementation of the `CordaRPCOps`_ that may be accessed with ``proxy``
are returned by RPCs can be subscribed to in order to receive an ongoing stream of updates from the node. More in Kotlin and ``getProxy()`` in Java. Observables that are returned by RPCs can be subscribed to in order to receive
detail on how to use this is provided in the docs for the proxy method. an ongoing stream of updates from the node. More detail on how to use this is provided in the docs for the proxy method.
.. warning:: The returned object is somewhat expensive to create and consumes a small amount of server side .. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of
resources. When you're done with it, cast it to ``Closeable`` or ``AutoCloseable`` and close it. Don't create server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use``
one for every call you make - create a proxy and reuse it. method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create
a new proxy for every call you make - reuse an existing one.
For a brief tutorial on how one can use the RPC API see :doc:`tutorial-clientrpc-api`. For a brief tutorial on how one can use the RPC API see :doc:`tutorial-clientrpc-api`.
@ -34,25 +35,21 @@ The returned observable may even emit object graphs with even more observables i
would expect. would expect.
This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you
download them. Therefore RPCs that use this feature are marked with the ``@RPCReturnsObservables`` annotation, and download them. Note that the server side observation buffer is bounded, once it fills up the client is considered
you are expected to subscribe to all the observables returned. If you don't want an observable then subscribe slow and kicked. You are expected to subscribe to all the observables returned, otherwise client-side memory starts
then unsubscribe immediately to clear the buffers and indicate that you aren't interested. If your app quits then filling up as observations come in. If you don't want an observable then subscribe then unsubscribe immediately to
server side resources will be freed automatically. clear the client-side buffers and to stop the server from streaming. If your app quits then server side resources
will be freed automatically.
When all the observables returned by an RPC are unsubscribed on the client side, that unsubscription propagates .. warning:: If you leak an observable on the client side and it gets garbage collected, you will get a warning
through to the server where the corresponding server-side observables are also unsubscribed. printed to the logs and the observable will be unsubscribed for you. But don't rely on this, as garbage collection
is non-deterministic.
.. warning:: If you leak an observable or proxy on the client side and it gets garbage collected, you will get
a warning printed to the logs and the proxy will be closed for you. But don't rely on this, as garbage
collection is non-deterministic.
Futures Futures
------- -------
A method can also return a ``ListenableFuture`` in its object graph and it will be treated in a similar manner to A method can also return a ``ListenableFuture`` in its object graph and it will be treated in a similar manner to
observables, including needing to mark the RPC with the ``@RPCReturnsObservables`` annotation. Unlike for an observable, observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release any resources.
once the single value (or an exception) has been received all server-side resources will be released automatically. Calling
the ``cancel`` method on the future will unsubscribe it from any future value and release any resources.
Versioning Versioning
---------- ----------
@ -66,13 +63,9 @@ of, an ``UnsupportedOperationException`` is thrown. If you want to know the vers
Thread safety Thread safety
------------- -------------
A proxy is thread safe, blocking, and will only allow a single RPC to be in flight at once. Any observables that A proxy is thread safe, blocking, and allows multiple RPCs to be in flight at once. Any observables that are returned and
are returned and you subscribe to will have objects emitted on a background thread. Observables returned as part you subscribe to will have objects emitted in order on a background thread pool. Each Observable stream is tied to a single
of one RPC and observables returned from another may have their callbacks invoked in parallel, but observables thread, however note that two separate Observables may invoke their respective callbacks on different threads.
returned as part of the same specific RPC invocation are processed serially and will not be invoked in parallel.
If you want to make multiple calls to the server in parallel you can do that by creating multiple proxies, but
be aware that the server itself may *not* process your work in parallel even if you make your requests that way.
Error handling Error handling
-------------- --------------
@ -85,8 +78,7 @@ side as if it was thrown from inside the called RPC method. These exceptions can
Wire protocol Wire protocol
------------- -------------
The client RPC wire protocol is not currently documented. To use it you must use the client library provided. The client RPC wire protocol is defined and documented in ``net/corda/client/rpc/RPCApi.kt``.
This is likely to change in a future release.
Whitelisting classes with the Corda node Whitelisting classes with the Corda node
---------------------------------------- ----------------------------------------
@ -98,5 +90,6 @@ with the annotation ``@CordaSerializable``. See :doc:`creating-a-cordapp` or :d
.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely. .. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely.
.. _CordaRPCClient: api/kotlin/corda/net.corda.client.rpc/-corda-r-p-c-client/index.html .. _CordaRPCClient: api/javadoc/net/corda/client/rpc/CordaRPCClient.html
.. _CordaRPCOps: api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html .. _CordaRPCOps: api/javadoc/net/corda/core/messaging/CordaRPCOps.html
.. _CordaRPCConnection: api/javadoc/net/corda/client/rpc/CordaRPCConnection.html

View File

@ -81,7 +81,8 @@ path to the node's base directory.
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However, .. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed
here must be externally accessible when running nodes across a cluster of machines. here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable,
the node will try to auto-discover its public one.
:rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC. :rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC.

View File

@ -45,19 +45,7 @@ extensions to be created, or registered at startup. In particular:
jars. These static serving directories will not be available if the jars. These static serving directories will not be available if the
bundled web server is not started. bundled web server is not started.
c. The ``requiredFlows`` property is used to declare new protocols in c. The ``servicePlugins`` property returns a list of classes which will
the plugin jar. Specifically the property must return a map with a key
naming each exposed top level flow class and a value which is a set
naming every parameter class that will be passed to the flow's
constructor. Standard ``java.lang.*`` and ``kotlin.*`` types do not need
to be included, but all other parameter types, or concrete interface
implementations need declaring. Declaring a specific flow in this map
white lists it for activation by the ``FlowLogicRefFactory``. White
listing is not strictly required for ``subFlows`` used internally, but
is required for any top level flow, or a flow which is invoked through
the scheduler.
d. The ``servicePlugins`` property returns a list of classes which will
be instantiated once during the ``AbstractNode.start`` call. These be instantiated once during the ``AbstractNode.start`` call. These
classes must provide a single argument constructor which will receive a classes must provide a single argument constructor which will receive a
``PluginServiceHub`` reference. They must also extend the abstract class ``PluginServiceHub`` reference. They must also extend the abstract class
@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular:
functions inside the node, for instance to initiate workflows when functions inside the node, for instance to initiate workflows when
certain conditions are met. certain conditions are met.
e. The ``customizeSerialization`` function allows classes to be whitelisted d. The ``customizeSerialization`` function allows classes to be whitelisted
for object serialisation, over and above those tagged with the ``@CordaSerializable`` for object serialisation, over and above those tagged with the ``@CordaSerializable``
annotation. In general the annotation should be preferred. For annotation. In general the annotation should be preferred. For
instance new state types will need to be explicitly registered. This will be called at instance new state types will need to be explicitly registered. This will be called at

View File

@ -12,11 +12,10 @@ App plugins
To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains 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: specific details of the implementation, but you can extend the server in the following ways:
1. Required flows: Specify which flows will be whitelisted for use in your RPC calls. 1. Service plugins: Register your services (see below).
2. Service plugins: Register your services (see below). 2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
3. 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. 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
5. 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. of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
Services Services

View File

@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events:
``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance. ``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance.
``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it ``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it
will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the
wallet, it can expect to be queried for the next activity when committed to the wallet. wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be
annotated with ``@SchedulableFlow``.
* If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself. * If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself.
The important thing to remember is that in the current implementation, each node that is party to the transaction The important thing to remember is that in the current implementation, each node that is party to the transaction
will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract
@ -90,10 +91,7 @@ business process and to take on those roles. That ``FlowLogic`` will be handed
rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered
for the interest rate before indicating an error. for the interest rate before indicating an error.
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to .. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate.
instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as
part of our overall security sandboxing.
As previously mentioned, we currently need a small network handler to assist with session setup until the work to As previously mentioned, we currently need a small network handler to assist with session setup until the work to
automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which

View File

@ -8,7 +8,6 @@ import net.corda.core.contracts.StateAndRef
import net.corda.core.contracts.TransactionType import net.corda.core.contracts.TransactionType
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.sign
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party import net.corda.core.identity.Party
@ -48,12 +47,12 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> { notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
// Collect cash type inputs // Collect cash type inputs
val cashStates = serviceHub.vaultService.unconsumedStates<Cash.State>() val cashStates = serviceHub.vaultService.unconsumedStates<Cash.State>()
// extract our key identity for convenience // extract our identity for convenience
val ourKey = serviceHub.myInfo.legalIdentity.owningKey val ourIdentity = serviceHub.myInfo.legalIdentity
// Filter down to our own cash states with right currency and issuer // Filter down to our own cash states with right currency and issuer
val suitableCashStates = cashStates.filter { val suitableCashStates = cashStates.filter {
val state = it.state.data val state = it.state.data
(state.owner == ourKey) (state.owner == ourIdentity)
&& (state.amount.token == amountRequired.token) && (state.amount.token == amountRequired.token)
} }
require(!suitableCashStates.isEmpty()) { "Insufficient funds" } require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
@ -90,12 +89,12 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary) val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
// Build and an output state for the counterparty // Build and an output state for the counterparty
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey) val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
if (residual > 0L) { if (residual > 0L) {
// Build an output state for the residual change back to us // Build an output state for the residual change back to us
val residualAmount = Amount(residual, sellAmount.token) val residualAmount = Amount(residual, sellAmount.token)
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey) val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity)
return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput)) return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
} else { } else {
return FxResponse(inputs, listOf(transferedFundsOutput)) return FxResponse(inputs, listOf(transferedFundsOutput))
@ -140,7 +139,7 @@ class ForeignExchangeFlow(val tradeId: String,
require(it.inputs.all { it.state.notary == notary }) { require(it.inputs.all { it.state.notary == notary }) {
"notary of remote states must be same as for our states" "notary of remote states must be same as for our states"
} }
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner.owningKey }) { require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) {
"The inputs are not owned by the correct counterparty" "The inputs are not owned by the correct counterparty"
} }
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) { require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
@ -153,7 +152,7 @@ class ForeignExchangeFlow(val tradeId: String,
>= remoteRequestWithNotary.amount.quantity) { >= remoteRequestWithNotary.amount.quantity) {
"the provided inputs don't provide sufficient funds" "the provided inputs don't provide sufficient funds"
} }
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity.owningKey }. require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity }.
map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) { map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) {
"the provided outputs don't provide the request quantity" "the provided outputs don't provide the request quantity"
} }
@ -195,8 +194,8 @@ class ForeignExchangeFlow(val tradeId: String,
val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary) val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary)
// Add the move commands and key to indicate all the respective owners and need to sign // Add the move commands and key to indicate all the respective owners and need to sign
val ourSigners = ourStates.inputs.map { it.state.data.owner }.toSet() val ourSigners = ourStates.inputs.map { it.state.data.owner.owningKey }.toSet()
val theirSigners = theirStates.inputs.map { it.state.data.owner }.toSet() val theirSigners = theirStates.inputs.map { it.state.data.owner.owningKey }.toSet()
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList()) builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
// Build and add the inputs and outputs // Build and add the inputs and outputs
@ -206,11 +205,9 @@ class ForeignExchangeFlow(val tradeId: String,
builder.withItems(*theirStates.outputs.toTypedArray()) builder.withItems(*theirStates.outputs.toTypedArray())
// We have already validated their response and trust our own data // We have already validated their response and trust our own data
// so we can sign // so we can sign. Note the returned SignedTransaction is still not fully signed
builder.signWith(serviceHub.legalIdentityKey) // and would not pass full verification yet.
// create a signed transaction, but pass false as parameter, because we know it is not fully signed return serviceHub.signInitialTransaction(builder)
val signedTransaction = builder.toSignedTransaction(checkSufficientSignatures = false)
return signedTransaction
} }
// DOCEND 3 // DOCEND 3
} }
@ -260,7 +257,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
} }
// assuming we have completed state and business level validation we can sign the trade // assuming we have completed state and business level validation we can sign the trade
val ourSignature = serviceHub.legalIdentityKey.sign(proposedTrade.id) val ourSignature = serviceHub.createSignature(proposedTrade)
// send the other side our signature. // send the other side our signature.
send(source, ourSignature) send(source, ourSignature)

View File

@ -2,9 +2,12 @@ package net.corda.docs
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.containsAny
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.PluginServiceHub import net.corda.core.node.PluginServiceHub
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
@ -64,10 +67,10 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState { override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState {
val parties: List<Party> get() = listOf(source, counterparty) val parties: List<Party> get() = listOf(source, counterparty)
override val participants: List<PublicKey> get() = parties.map { it.owningKey } override val participants: List<AbstractParty> get() = parties
override fun isRelevant(ourKeys: Set<PublicKey>): Boolean { override fun isRelevant(ourKeys: Set<PublicKey>): Boolean {
return participants.any { it.containsAny(ourKeys) } return participants.any { it.owningKey.containsAny(ourKeys) }
} }
} }
@ -131,9 +134,7 @@ class SubmitTradeApprovalFlow(val tradeId: String,
.withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey))) .withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey)))
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
// We can automatically sign as there is no untrusted data. // We can automatically sign as there is no untrusted data.
tx.signWith(serviceHub.legalIdentityKey) val signedTx = serviceHub.signInitialTransaction(tx)
// Convert to a SignedTransaction that we can send to the notary
val signedTx = tx.toSignedTransaction(false)
// Notarise and distribute. // Notarise and distribute.
subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty))) subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty)))
// Return the initial state // Return the initial state
@ -195,9 +196,9 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60)) tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
// We can sign this transaction immediately as we have already checked all the fields and the decision // 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. // is ultimately a manual one from the caller.
tx.signWith(serviceHub.legalIdentityKey) // As a SignedTransaction we can pass the data around certain that it cannot be modified,
// Convert to SignedTransaction we can pass around certain that it cannot be modified. // although we do require further signatures to complete the process.
val selfSignedTx = tx.toSignedTransaction(false) val selfSignedTx = serviceHub.signInitialTransaction(tx)
//DOCEND 2 //DOCEND 2
// Send the signed transaction to the originator and await their signature to confirm // Send the signed transaction to the originator and await their signature to confirm
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(newState.source, selfSignedTx).unwrap { val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(newState.source, selfSignedTx).unwrap {
@ -253,7 +254,7 @@ class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
} }
// DOCEND 3 // DOCEND 3
// Having verified the SignedTransaction passed to us we can sign it too // Having verified the SignedTransaction passed to us we can sign it too
val ourSignature = serviceHub.legalIdentityKey.sign(completeTx.tx.id) val ourSignature = serviceHub.createSignature(completeTx)
// Send our signature to the other party. // Send our signature to the other party.
send(source, ourSignature) send(source, ourSignature)
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction // N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction

View File

@ -8,7 +8,6 @@ import net.corda.core.toFuture
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.DUMMY_NOTARY_KEY import net.corda.core.utilities.DUMMY_NOTARY_KEY
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.services.network.NetworkMapService import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.transactions.ValidatingNotaryService import net.corda.node.services.transactions.ValidatingNotaryService
import net.corda.node.utilities.transaction import net.corda.node.utilities.transaction

View File

@ -0,0 +1,63 @@
Flow Library
============
There are a number of built-in flows supplied with Corda, which cover some core functionality.
FinalityFlow
------------
The ``FinalityFlow`` verifies the given transactions, then sends them to the specified notary.
If the notary agrees that the transactions are acceptable then they are from that point onwards committed to the ledger,
and will be written through to the vault. Additionally they will be distributed to the parties reflected in the participants
list of the states.
The transactions will be topologically sorted before commitment to ensure that dependencies are committed before
dependers, so you don't need to do this yourself.
The transactions are expected to have already been resolved: if their dependencies are not available in local storage or
within the given set, verification will fail. They must have signatures from all necessary parties other than the notary.
If specified, the extra recipients are sent all the given transactions. The base set of parties to inform of each
transaction are calculated on a per transaction basis from the contract-given set of participants.
The flow returns the same transactions, in the same order, with the additional signatures.
CollectSignaturesFlow
---------------------
The ``CollectSignaturesFlow`` is used to automate the collection of signatures from the counter-parties to a transaction.
You use the ``CollectSignaturesFlow`` by passing it a ``SignedTransaction`` which has at least been signed by yourself.
The flow will handle the resolution of the counter-party identities and request a signature from each counter-party.
Finally, the flow will verify all the signatures and return a ``SignedTransaction`` with all the collected signatures.
When using this flow on the responding side you will have to subclass the ``AbstractCollectSignaturesFlowResponder`` and
provide your own implementation of the ``checkTransaction`` method. This is to add additional verification logic on the
responder side. Types of things you will need to check include:
* Ensuring that the transaction you are receiving is the transaction you *EXPECT* to receive. I.e. is has the expected
type of inputs and outputs
* Checking that the properties of the outputs are as you would expect, this is in the absence of integrating reference
data sources to facilitate this for us
* Checking that the transaction is not incorrectly spending (perhaps maliciously) one of your asset states, as potentially
the transaction creator has access to some of your state references
Typically after calling the ``CollectSignaturesFlow`` you then called the ``FinalityFlow``.
ResolveTransactionsFlow
-----------------------
This ``ResolveTransactionsFlow`` is used to verify the validity of a transaction by recursively checking the validity of
all the dependencies. Once a transaction is checked it's inserted into local storage so it can be relayed and won't be
checked again.
A couple of constructors are provided that accept a single transaction. When these are used, the dependencies of that
transaction are resolved and then the transaction itself is verified. Again, if successful, the results are inserted
into the database as long as a [SignedTransaction] was provided. If only the ``WireTransaction`` form was provided
then this isn't enough to put into the local database, so only the dependencies are checked and inserted. This way
to use the flow is helpful when resolving and verifying an unfinished transaction.
The flow returns a list of verified ``LedgerTransaction`` objects, in a depth-first order.

View File

@ -131,7 +131,7 @@ each side.
val notaryNode: NodeInfo, val notaryNode: NodeInfo,
val assetToSell: StateAndRef<OwnableState>, val assetToSell: StateAndRef<OwnableState>,
val price: Amount<Currency>, val price: Amount<Currency>,
val myKeyPair: KeyPair, val myKey: PublicKey,
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() { override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
@ -160,7 +160,8 @@ Going through the data needed to become a seller, we have:
information on notaries. information on notaries.
- ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold. - ``assetToSell: StateAndRef<OwnableState>`` - a pointer to the ledger entry that represents the thing being sold.
- ``price: Amount<Currency>`` - the agreed on price that the asset is being sold for (without an issuer constraint). - ``price: Amount<Currency>`` - the agreed on price that the asset is being sold for (without an issuer constraint).
- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction. - ``myKey: PublicKey`` - the PublicKey part of the node's internal KeyPair that controls the asset being sold.
The matching PrivateKey stored in the KeyManagementService will be used to sign the transaction.
And for the buyer: And for the buyer:
@ -206,9 +207,10 @@ how to register handlers with the messaging system (see ":doc:`messaging`") and
when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network
interaction and it will save/restore serialised versions of the fiber at the right times. interaction and it will save/restore serialised versions of the fiber at the right times.
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events, Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered
APIs from your app code. directly via the node's RPC API from your app code (in which case they need to be annotated with `StartableByRPC`). It's
possible for a flow to be of both types.
You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a
Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``). Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``).
@ -399,15 +401,35 @@ This code is longer but no more complicated. Here are some things to pay attenti
As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite 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. the fact that it takes minimal resources and can survive node restarts.
Initiating communication Flow sessions
------------------------ -------------
Now that we have both sides of the deal negotation implemented as flows we need a way to start things off. We do this by Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows
having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore
RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes
it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller`` do not interfere with each other.
are annotated with it. For example, if we choose the seller side as the initiator then we need a seller starter flow that
might look something like this: To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party``
for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to
the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party``
will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends
pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known
sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is
``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate.
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication
channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is
making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp
which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method
specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist
then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be
annotated with ``InitiatingFlow``.
Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done
with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side.
In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor
``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need
to create a simple seller starter flow that has the annotation we need:
.. container:: codeset .. container:: codeset
@ -418,7 +440,7 @@ might look something like this:
@Suspendable @Suspendable
override fun call(): SignedTransaction { override fun call(): SignedTransaction {
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0] val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
val cpOwnerKey: KeyPair = serviceHub.legalIdentityKey val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey
return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey)) return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey))
} }
} }

View File

@ -4,63 +4,142 @@ Getting set up
Software requirements Software requirements
--------------------- ---------------------
Corda uses industry-standard tools to make set-up as simple as possible. Following the software recommendations below will 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.
minimize the number of errors you encounter, and make it easier for others to provide support. However, if you do use other tools,
we're interested to hear about any issues that arise.
JVM JVM
~~~ ~~~
Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively supported.
supported. Oracle JDK 8 can be obtained directly from
`Oracle <http://www.oracle.com/technetwork/java/javase/downloads/index.html>`_. Installation instructions are
available for `Windows <http://docs.oracle.com/javase/8/docs/technotes/guides/install/windows_jdk_install.html#CHDEBCCJ>`_,
`Linux <http://docs.oracle.com/javase/8/docs/technotes/guides/install/linux_jdk.html#BJFGGEFG>`_ and
`OS X <http://docs.oracle.com/javase/8/docs/technotes/guides/install/mac_jdk.html#CHDBADCG>`_.
Please ensure that you keep your Oracle JDK installation updated to the latest version while working with Corda. 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.
Even earlier versions of JDK 8 versions can cause cryptic errors.
If you do choose to use OpenJDK instead of Oracle's JDK, you will also need to install OpenJFX. If you do choose to use OpenJDK instead of Oracle's JDK, you will also need to install OpenJFX.
Additional troubleshooting information can be found `here <https://docs.corda.net/getting-set-up-fault-finding.html#java-issues>`_.
Kotlin Kotlin
~~~~~~ ~~~~~~
Applications on Corda (CorDapps) can be written in any JVM-targeting language. However, Corda itself and most of the samples 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 <https://kotlinlang.org/docs/tutorials/>`_.
are written in Kotlin. If you're unfamiliar with Kotlin, there is an official `getting started guide <https://kotlinlang.org/docs/tutorials/>`_.
See also our :doc:`further-notes-on-kotlin`. See also our :doc:`further-notes-on-kotlin`.
IDE IDE
~~~ ~~~
We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration. The free Community We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration.
Edition can be downloaded from `JetBrains <https://www.jetbrains.com/idea/download/>`_.
Please make sure that you're running the latest version of IDEA, as older versions have been known to have problems integrating with Gradle, 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.
the build tool used by Corda.
You'll also want to install the Kotlin IDEA plugin by following the instructions
`here <https://kotlinlang.org/docs/tutorials/getting-started.html>`_.
Additional troubleshooting information can be found `here <https://docs.corda.net/getting-set-up-fault-finding.html#idea-issues>`_.
Git Git
~~~ ~~~
We use git to version-control Corda. Instructions on installing git can be found We use git to version-control Corda.
`here <https://git-scm.com/book/en/v2/Getting-Started-Installing-Git>`_.
Following these instructions will give you access to git via the command line. It can also be useful to control git via IDEA. Instructions
for doing so can be found on the `JetBrains website <https://www.jetbrains.com/help/idea/2016.2/using-git-integration.html>`_.
Gradle Gradle
~~~~~~ ~~~~~~
We use Gradle as the build tool for Corda. However, you do not need to install Gradle itself, as a wrapper is provided. We use Gradle as the build tool for Corda. However, you do not need to install Gradle itself, as a wrapper is provided.
The wrapper can be run from the command line by using ``./gradlew [taskName]`` on OS X/Linux, or ``gradlew.bat [taskName]`` on Windows. Set-up instructions
-------------------
The instructions below will allow you to set up a Corda development environment and run a basic CorDapp on a Windows or Mac machine. If you have any issues, please consult the :doc:`getting-set-up-fault-finding` page, or reach out on `Slack <http://slack.corda.net/>`_ or the `forums <https://discourse.corda.net/>`_.
.. note:: The set-up instructions are also available in video form for both `Windows <https://vimeo.com/217462250>`_ and `Mac <https://vimeo.com/217462230>`_.
Windows
~~~~~~~
Java
""""
1. Visit http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html
2. Scroll down to "Java SE Development Kit 8uXXX" (where "XXX" is the latest minor version number)
3. Toggle "Accept License Agreement"
4. Click the download link for jdk-8uXXX-windows-x64.exe (where "XXX" is the latest minor version number)
5. Download and run the executable to install Java (use the default settings)
6. Open a new command prompt and run ``java -version`` to test that Java is installed correctly
Git
"""
1. Visit https://git-scm.com/download/win
2. Click the "64-bit Git for Windows Setup" download link.
3. Download and run the executable to install Git (use the default settings)
4. Open a new command prompt and type ``git --version`` to test that git is installed correctly
IntelliJ
""""""""
1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?code=IIC
2. Download and run the executable to install IntelliJ Community Edition (use the default settings)
Download a sample project
"""""""""""""""""""""""""
1. Open a command prompt
2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial``
3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial``
4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*``
5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone)
Run from the command prompt
"""""""""""""""""""""""""""
1. From the cordapp-tutorial folder, deploy the nodes by running ``gradlew deployNodes``
2. Start the nodes by running ``call kotlin-source/build/nodes/runnodes.bat``
3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec"
4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/
Run from IntelliJ
"""""""""""""""""
1. Open IntelliJ Community Edition
2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-template folder
.. warning:: If you click "Import Project" instead of "Open", the project's run configurations will be erased!
3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to C:\Program Files\Java\jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK".
4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears
5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete)
6. At the top-right of the screen, to the left of the green "play" arrow, you should see a dropdown. In that dropdown, select "Run Example Cordapp - Kotlin" and click the green "play" arrow.
7. Wait until the run windows displays the message "Webserver started up in XX.X sec"
8. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/
Mac
~~~
Java
""""
1. Open "System Preferences > Java"
2. In the Java Control Panel, if an update is available, click "Update Now"
3. In the "Software Update" window, click "Install Update". If required, enter your password and click "Install Helper" when prompted
4. Wait for a pop-up window indicating that you have successfully installed the update, and click "Close"
5. Open a new terminal and type ``java -version`` to test that Java is installed correctly
IntelliJ
""""""""
1. Visit https://www.jetbrains.com/idea/download/download-thanks.html?platform=mac&code=IIC
2. Download and run the executable to install IntelliJ Community Edition (use the default settings)
Download a sample project
"""""""""""""""""""""""""
1. Open a terminal
2. Clone the CorDapp tutorial repo by running ``git clone https://github.com/corda/cordapp-tutorial``
3. Move into the cordapp-tutorial folder by running ``cd cordapp-tutorial``
4. Retrieve a list of all the milestone (i.e. stable) releases by running ``git branch -a --list *release-M*``
5. Check out the latest milestone release by running ``git checkout release-MX`` (where "X" is the latest milestone)
Run from the terminal
"""""""""""""""""""""
1. From the cordapp-tutorial folder, deploy the nodes by running ``./gradlew deployNodes``
2. Start the nodes by running ``kotlin-source/build/nodes/runnodes``. Do not click while 8 additional terminal windows start up.
3. Wait until all the terminal windows display either "Webserver started up in XX.X sec" or "Node for "NodeC" started up and registered in XX.XX sec"
4. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/
Run from IntelliJ
"""""""""""""""""
1. Open IntelliJ Community Edition
2. On the splash screen, click "Open" (do NOT click "Import Project") and select the cordapp-template folder
3. Once the project is open, click "File > Project Structure". Under "Project SDK:", set the project SDK by clicking "New...", clicking "JDK", and navigating to /Library/Java/JavaVirtualMachines/jdk1.8.0_XXX (where "XXX" is the latest minor version number). Click "OK".
4. Click "View > Tool Windows > Event Log", and click "Import Gradle project", then "OK". Wait, and click "OK" again when the "Gradle Project Data To Import" window appears
5. Wait for indexing to finish (a progress bar will display at the bottom-right of the IntelliJ window until indexing is complete)
6. At the top-right of the screen, to the left of the green "play" arrow, you should see a dropdown. In that dropdown, select "Run Example Cordapp - Kotlin" and click the green "play" arrow.
7. Wait until the run windows displays the message "Webserver started up in XX.X sec"
8. Test the CorDapp is running correctly by visiting the front end at http://localhost:10007/web/example/
Corda source code Corda source code
----------------- -----------------
@ -79,25 +158,7 @@ And a simple example CorDapp for you to explore basic concepts is available here
You can clone these repos to your local machine by running the command ``git clone [repo URL]``. You can clone these repos to your local machine by running the command ``git clone [repo URL]``.
By default, these repos will be on the ``master`` branch. However, this is an unstable development branch. You should check 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``.
out the latest release tag instead by running ``git checkout release-M10.1``.
Opening Corda/CorDapps in IDEA
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. warning:: If you choose to use IntelliJ you must run the ``gradlew kaptKotlin`` task before attempting to compile via IntelliJ.
.. note:: If you change branch , gradle clean or see a compile error in ``VaultSchemaTest.kt`` you must also then re-run `gradlew kaptKotlin`
When opening a Corda project for the first time from the IDEA splash screen, please click "Open" rather than "Import Project",
and then import the Gradle project by clicking "Import Gradle project" in the popup bubble on the lower right-hand side of the screen.
If you instead pick "Import Project" on the splash screen, a bug in IDEA will cause Corda's pre-packaged run configurations to be erased.
If you see this warning too late, that's not a problem - just use ``git checkout .idea/runConfigurations`` or the version control tab in
IDEA to undelete the files.
IDEA's build of the project may need to be resynced from time to time. This can be done from within IDEA by going to "View" -> "Tool Windows" -> "Gradle"
and clicking "Refresh all Gradle projects". Whenever prompted about Gradle, accept the defaults suggested by IDEA.
Next steps Next steps
---------- ----------

View File

@ -2,10 +2,10 @@ Welcome to the Corda documentation!
=================================== ===================================
.. warning:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the .. warning:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the
current state of the code. `Read the docs for milestone release M10.1 <https://docs.corda.net/releases/release-M10.1/>`_. current state of the code. `Read the docs for milestone release M11.1 <https://docs.corda.net/releases/release-M11.1/>`_.
`Corda <https://www.corda.net/>`_ is an open-source distributed ledger platform. The latest *milestone* (i.e. stable) `Corda <https://www.corda.net/>`_ is an open-source distributed ledger platform. The latest *milestone* (i.e. stable)
release is M10.1. The codebase is on `GitHub <https://github.com/corda>`_, and our community can be found on release is M11.1. The codebase is on `GitHub <https://github.com/corda>`_, and our community can be found on
`Slack <https://slack.corda.net/>`_ and in our `forum <https://discourse.corda.net/>`_. `Slack <https://slack.corda.net/>`_ and in our `forum <https://discourse.corda.net/>`_.
If you're new to Corda, you should start by learning about its motivating vision and architecture. A good introduction If you're new to Corda, you should start by learning about its motivating vision and architecture. A good introduction
@ -118,6 +118,7 @@ Documentation Contents:
:maxdepth: 2 :maxdepth: 2
:caption: Component library :caption: Component library
flow-library
contract-catalogue contract-catalogue
contract-irs contract-irs

View File

@ -55,10 +55,9 @@ resolving the attachment references to the attachments. Commands with valid sign
When constructing a new transaction from scratch, you use ``TransactionBuilder``, which is a mutable transaction that 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 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. (before signature, before verification). It is intended to be passed around code that may edit it by adding new states/commands.
Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties. Then once the states and commands are right then an initial DigitalSignature.WithKey can be added to freeze the transaction data.
It is typical for contract classes to expose helper methods that can contribute to a ``TransactionBuilder``. Once a transaction Typically, the signInitialTransaction method on the flow's serviceHub object will be used to look up the default node identity PrivateKey,
has been constructed using the builders ``toWireTransaction`` or ``toSignedTransaction`` function, it shared with other sign the transaction and return a partially signed SignedTransaction. This can then be distributed to other participants using the :doc:`key-concepts-flow-framework`.
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 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): contract type in the library):
@ -69,10 +68,9 @@ contract type in the library):
val notaryToUse: Party = ... val notaryToUse: Party = ...
val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia")) val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia"))
txb.signWith(myKey)
txb.setTime(Instant.now(), notaryToUse, 30.seconds) txb.setTime(Instant.now(), notaryToUse, 30.seconds)
// We must disable the check for sufficient signatures, because this transaction is not yet notarised. // Carry out the initial signing of the transaction and creation of a (partial) SignedTransation.
val stx = txb.toSignedTransaction(checkSufficientSignatures = false) val stx = serviceHub.signInitialTransaction(txb)
// Alternatively, let's just check it verifies pretending it was fully signed. To do this, we get // 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 // a WireTransaction, which is what the SignedTransaction wraps. Thus by verifying that directly we
// skip signature checking. // skip signature checking.

View File

@ -1,21 +1,14 @@
Overview Overview
======== ========
This section describes the fundamental concepts and features that underpin the Corda platform, to include: This section describes the key concepts and features of the Corda platform.
* :doc:`key-concepts-ecosystem` The detailed thinking and rationale behind these concepts are presented in two white papers:
* :doc:`key-concepts-data-model`
* :doc:`key-concepts-core-types`
* :doc:`key-concepts-financial-model`
* :doc:`key-concepts-flow-framework`
* :doc:`key-concepts-consensus-notaries`
* :doc:`key-concepts-vault`
* :doc:`key-concepts-security-model`
Detailed thinking and rationale behind these concepts are presented in the following published white papers:
* `Corda: An Introduction`_ * `Corda: An Introduction`_
* `Corda: A Distributed Ledger`_ (Technical White Paper) * `Corda: A Distributed Ledger`_ (A.K.A. the Technical White Paper)
Explanations of the key concepts are also available as `videos <https://vimeo.com/album/4555732/>`_.
.. _`Corda: An Introduction`: _static/corda-introductory-whitepaper.pdf .. _`Corda: An Introduction`: _static/corda-introductory-whitepaper.pdf
.. _`Corda: A Distributed Ledger`: _static/corda-technical-whitepaper.pdf .. _`Corda: A Distributed Ledger`: _static/corda-technical-whitepaper.pdf

View File

@ -65,12 +65,12 @@ PersistentKeyManagementService and E2ETestKeyManagementService
Typical usage of these services is to locate an appropriate Typical usage of these services is to locate an appropriate
``PrivateKey`` to complete and sign a verified transaction as part of a ``PrivateKey`` to complete and sign a verified transaction as part of a
flow. The normal node legal identifier keys are typically accessed via flow. The normal node legal identifier keys are typically accessed via
helper extension methods on the ``ServiceHub``, but these ultimately helper extension methods on the ``ServiceHub``, but these ultimately delegate
fetch the keys from the ``KeyManagementService``. The signing to internal ``PrivateKeys`` from the ``KeyManagementService``. The
``KeyManagementService`` interface also allows other keys to be ``KeyManagementService`` interface also allows other keys to be
generated if anonymous keys are needed in a flow. Note that this generated if anonymous keys are needed in a flow. Note that this
interface works at the level of individual ``PublicKey``/``PrivateKey`` interface works at the level of individual ``PublicKey`` and internally
pairs, but the signing authority will be represented by a matched ``PrivateKey` pairs, but the signing authority may be represented by a
``CompositeKey`` on the ``NodeInfo`` to allow key clustering and ``CompositeKey`` on the ``NodeInfo`` to allow key clustering and
threshold schemes. threshold schemes.

View File

@ -168,11 +168,12 @@ Let's see what parameters we pass to the constructor of this oracle.
.. sourcecode:: kotlin .. sourcecode:: kotlin
class Oracle(val identity: Party, private val signingKey: KeyPair, val clock: Clock) = TODO() class Oracle(val identity: Party, private val signingKey: PublicKey, val clock: Clock) = TODO()
Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to
sign for, and also needs a pair of signing keys with which it signs transactions. The clock is used for the deadline sign for, and also needs the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService
functionality which we will not discuss further here. to identify the internal PrivateKey used for transaction signing.
The clock is used for the deadline functionality which we will not discuss further here.
Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the
parameter and ``CommandData`` classes. parameter and ``CommandData`` classes.

View File

@ -10,7 +10,8 @@ We've added the ability for flows to be versioned by their CorDapp developers. T
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future 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. release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
There are major changes to the ``Party`` class as part of confidential identities. See :doc:`changelog` for full details. There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored
in transaction state objects. See :doc:`changelog` for full details.
Milestone 11 Milestone 11

View File

@ -1,48 +0,0 @@
Release process
===============
Corda is under heavy development. The current release process is therefore geared towards rapid iteration.
Each Corda development release is called a *milestone* and has its own branch in the git repository. Milestones are
temporarily stabilised snapshots of the Corda code which are suitable for developers to experiment with. They may
receive backported bugfixes but once announced a milestone will not have any API or backwards compatibility breaks.
Between milestones backwards compatibility is expected to break. Every new milestone comes with a short announcement
detailing:
* What major improvements have been made.
* How to forward port your code to the new milestone.
* What new documentation has become available.
* Important known issues.
Eventually, Corda will stabilise and release version 1. At that point backwards compatibility will be guaranteed
forever and the software will be considered production ready. Until then, expect it to be a building site and wear your
hard hat.
Our goal is to cut a new milestone roughly once a month. There are no fixed dates. If need be, a milestone may slip by
a few days to ensure the code is sufficiently usable. Usually the release will happen around the end of the month.
Steps to cut a release
----------------------
1. Pick a commit that is stable and do basic QA: run all the tests, run the demos.
2. Review the commits between this release and the last looking for new features, API changes, etc. Make sure the
summary in the current section of the :doc:`changelog` is correct and update if not. Then move it into the right
section for this release.
3. Write up a summary of the changes for the :doc:`release-notes`. This should primarily be suited to a semi-technical
audience, but any advice on how to port app code from the previous release, configuration changes required, etc.
should also go here.
4. Additionally, if there are any new features or APIs that deserve a new section in the docsite and the author didn't
create one, bug them to do so a day or two before the release.
5. Regenerate the docsite if necessary and commit.
6. Create a branch with a name like `release-M0` where 0 is replaced by the number of the milestone.
7. Adjust the version in the root build.gradle file to take out the -SNAPSHOT and commit it on the branch.
8. Remove the "is master" warning from the docsite index page on this branch only.
9. Tag the branch with a tag like `release-M0.0`
10. Push the branch and the tag to git.
11. Write up a short announcement containing the summary of new features, changes, and API breaks.
This can often be derived from the release notes. Send it to the r3dlg-awg mailing list.
12. On master, adjust the version number in the root build.gradle file upwards.
If there are serious bugs found in the release, backport the fix to the branch and then tag it with e.g. `release-M0.1`
Minor changes to the branch don't have to be announced unless it'd be critical to get all developers updated.

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Some files were not shown because too many files have changed in this diff Show More