mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'open/master'
This commit is contained in:
commit
376a9d399f
3
.gitignore
vendored
3
.gitignore
vendored
@ -32,6 +32,7 @@ lib/dokka.jar
|
||||
.idea/libraries
|
||||
.idea/shelf
|
||||
.idea/dataSources
|
||||
/gradle-plugins/.idea
|
||||
|
||||
# Include the -parameters compiler option by default in IntelliJ required for serialization.
|
||||
!.idea/compiler.xml
|
||||
@ -53,6 +54,7 @@ lib/dokka.jar
|
||||
# Gradle:
|
||||
# .idea/gradle.xml
|
||||
# .idea/libraries
|
||||
/gradle-plugins/gradle*
|
||||
|
||||
# Mongo Explorer plugin:
|
||||
# .idea/mongoSettings.xml
|
||||
@ -65,6 +67,7 @@ lib/dokka.jar
|
||||
|
||||
# IntelliJ
|
||||
/out/
|
||||
/classes/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
5
.idea/compiler.xml
generated
5
.idea/compiler.xml
generated
@ -17,6 +17,8 @@
|
||||
<module name="corda-webserver_integrationTest" target="1.8" />
|
||||
<module name="corda-webserver_main" 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_test" target="1.8" />
|
||||
<module name="demobench_main" target="1.8" />
|
||||
@ -59,10 +61,13 @@
|
||||
<module name="node_integrationTest" target="1.8" />
|
||||
<module name="node_main" 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_test" target="1.8" />
|
||||
<module name="rpc_integrationTest" 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="samples_main" target="1.8" />
|
||||
<module name="samples_test" target="1.8" />
|
||||
|
10
build.gradle
10
build.gradle
@ -16,6 +16,11 @@ buildscript {
|
||||
// TODO: Sort this alphabetically.
|
||||
ext.kotlin_version = constants.getProperty("kotlinVersion")
|
||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||
|
||||
// gradle-capsule-plugin:1.0.2 contains capsule:1.0.1
|
||||
// TODO: Upgrade gradle-capsule-plugin to a version with capsule:1.0.3
|
||||
ext.capsule_version = '1.0.1'
|
||||
|
||||
ext.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.5.3'
|
||||
ext.jackson_version = '2.8.5'
|
||||
@ -41,6 +46,7 @@ buildscript {
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.2.1'
|
||||
ext.dokka_version = '0.9.13'
|
||||
ext.eddsa_version = '0.2.0'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
ext.java8_minUpdateVersion = '131'
|
||||
@ -60,12 +66,14 @@ buildscript {
|
||||
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
|
||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
||||
classpath "org.ajoberstar:grgit:1.1.0"
|
||||
classpath "net.i2p.crypto:eddsa:$eddsa_version" // Needed for ServiceIdentityGenerator in the build environment.
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
// TODO The capsule plugin requires the newer DSL plugin block.It would be nice if we could unify all the plugins into one style,
|
||||
// but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle.
|
||||
// Version 1.0.2 of this plugin uses capsule:1.0.1
|
||||
id "us.kirchmeier.capsule" version "1.0.2"
|
||||
}
|
||||
|
||||
@ -249,7 +257,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
|
||||
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
|
@ -10,6 +10,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
import net.corda.core.contracts.Amount
|
||||
import net.corda.core.contracts.BusinessCalendar
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
@ -43,18 +44,21 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
|
||||
}
|
||||
|
||||
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
|
||||
}
|
||||
|
||||
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
|
||||
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
|
||||
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
|
||||
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
|
||||
@ -66,6 +70,7 @@ object JacksonSupport {
|
||||
addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer)
|
||||
addSerializer(Party::class.java, PartySerializer)
|
||||
addDeserializer(Party::class.java, PartyDeserializer)
|
||||
addDeserializer(AbstractParty::class.java, PartyDeserializer)
|
||||
addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||
addDeserializer(BigDecimal::class.java, NumberDeserializers.BigDecimalDeserializer())
|
||||
addSerializer(SecureHash::class.java, SecureHashSerializer)
|
||||
@ -160,8 +165,20 @@ object JacksonSupport {
|
||||
}
|
||||
|
||||
val mapper = parser.codec as PartyObjectMapper
|
||||
val principal = X500Name(parser.text)
|
||||
return mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
|
||||
// TODO: We should probably have a better specified way of identifying X.500 names vs keys
|
||||
// Base58 keys never include an equals character, while X.500 names always will, so we use that to determine
|
||||
// how to parse the content
|
||||
return if (parser.text.contains("=")) {
|
||||
val principal = X500Name(parser.text)
|
||||
mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
|
||||
} else {
|
||||
val key = try {
|
||||
parsePublicKeyBase58(parser.text)
|
||||
} catch (e: Exception) {
|
||||
throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key")
|
||||
}
|
||||
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -193,8 +193,7 @@ open class StringToMethodCallParser<in T : Any> @JvmOverloads constructor(
|
||||
val parameterString = "{ $args }"
|
||||
val tree: JsonNode = om.readTree(parameterString) ?: throw UnparseableCallException(args)
|
||||
if (tree.size() > parameters.size) throw UnparseableCallException.TooManyParameters(methodNameHint, args)
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { _, param ->
|
||||
val (argName, argType) = param
|
||||
val inOrderParams: List<Any?> = parameters.mapIndexed { _, (argName, argType) ->
|
||||
val entry = tree[argName] ?: throw UnparseableCallException.MissingParameter(methodNameHint, argName, args)
|
||||
try {
|
||||
om.readValue(entry.traverse(om), argType)
|
||||
|
@ -1,14 +1,16 @@
|
||||
package net.corda.jackson
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Test
|
||||
import kotlin.reflect.full.primaryConstructor
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class StringToMethodCallParserTest {
|
||||
@Suppress("UNUSED")
|
||||
class Target {
|
||||
fun simple() = "simple"
|
||||
fun string(note: String) = note
|
||||
fun string(noteTextWord: String) = noteTextWord
|
||||
fun twoStrings(a: String, b: String) = a + b
|
||||
fun simpleObject(hash: SecureHash.SHA256) = hash.toString()
|
||||
fun complexObject(pair: Pair<Int, String>) = pair
|
||||
@ -20,7 +22,7 @@ class StringToMethodCallParserTest {
|
||||
val randomHash = "361170110f61086f77ff2c5b7ab36513705da1a3ebabf14dbe5cc9c982c45401"
|
||||
val tests = mapOf(
|
||||
"simple" to "simple",
|
||||
"string note: A test of barewords" to "A test of barewords",
|
||||
"string noteTextWord: A test of barewords" to "A test of barewords",
|
||||
"twoStrings a: Some words, b: ' and some words, like, Kirk, would, speak'" to "Some words and some words, like, Kirk, would, speak",
|
||||
"simpleObject hash: $randomHash" to randomHash.toUpperCase(),
|
||||
"complexObject pair: { first: 12, second: Word up brother }" to Pair(12, "Word up brother"),
|
||||
@ -36,4 +38,31 @@ class StringToMethodCallParserTest {
|
||||
assertEquals(output, parser.parse(target, input).invoke())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNUSED")
|
||||
class ConstructorTarget(val someWord: String, val aDifferentThing: Int) {
|
||||
constructor(alternativeWord: String) : this(alternativeWord, 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun ctor1() {
|
||||
val clazz = ConstructorTarget::class.java
|
||||
val parser = StringToMethodCallParser(clazz)
|
||||
val ctor = clazz.constructors.single { it.parameterCount == 2 }
|
||||
val names: List<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!"))
|
||||
}
|
||||
}
|
@ -11,6 +11,9 @@ configurations {
|
||||
|
||||
integrationTestCompile.extendsFrom testCompile
|
||||
integrationTestRuntime.extendsFrom testRuntime
|
||||
|
||||
smokeTestCompile.extendsFrom compile
|
||||
smokeTestRuntime.extendsFrom runtime
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
@ -21,6 +24,24 @@ sourceSets {
|
||||
srcDir file('src/integration-test/kotlin')
|
||||
}
|
||||
}
|
||||
smokeTest {
|
||||
kotlin {
|
||||
// We must NOT have any Node code on the classpath, so do NOT
|
||||
// include the test or integrationTest dependencies here.
|
||||
compileClasspath += main.output
|
||||
runtimeClasspath += main.output
|
||||
srcDir file('src/smoke-test/kotlin')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processSmokeTestResources {
|
||||
from(file("$rootDir/config/test/log4j2.xml")) {
|
||||
rename 'log4j2\\.xml', 'log4j2-test.xml'
|
||||
}
|
||||
from(project(':node:capsule').tasks.buildCordaJAR) {
|
||||
rename 'corda-(.*)', 'corda.jar'
|
||||
}
|
||||
}
|
||||
|
||||
// To find potential version conflicts, run "gradle htmlDependencyReport" and then look in
|
||||
@ -38,11 +59,22 @@ dependencies {
|
||||
testCompile project(':test-utils')
|
||||
testCompile project(':client:mock')
|
||||
|
||||
// Integration test helpers
|
||||
integrationTestCompile "junit:junit:$junit_version"
|
||||
// Smoke tests do NOT have any Node code on the classpath!
|
||||
smokeTestCompile project(':finance')
|
||||
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
|
||||
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
|
||||
smokeTestCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
|
||||
smokeTestCompile "org.assertj:assertj-core:${assertj_version}"
|
||||
smokeTestCompile "junit:junit:$junit_version"
|
||||
}
|
||||
|
||||
task integrationTest(type: Test) {
|
||||
testClassesDir = sourceSets.integrationTest.output.classesDir
|
||||
classpath = sourceSets.integrationTest.runtimeClasspath
|
||||
}
|
||||
|
||||
task smokeTest(type: Test) {
|
||||
testClassesDir = sourceSets.smokeTest.output.classesDir
|
||||
classpath = sourceSets.smokeTest.runtimeClasspath
|
||||
systemProperties['build.dir'] = buildDir
|
||||
}
|
||||
|
@ -5,26 +5,181 @@ import com.esotericsoftware.kryo.Serializer
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.pool.KryoPool
|
||||
import com.google.common.net.HostAndPort
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import net.corda.client.rpc.internal.RPCClient
|
||||
import net.corda.client.rpc.internal.RPCClientConfiguration
|
||||
import net.corda.core.*
|
||||
import net.corda.core.messaging.RPCOps
|
||||
import net.corda.core.millis
|
||||
import net.corda.core.random63BitValue
|
||||
import net.corda.node.driver.poll
|
||||
import net.corda.node.services.messaging.RPCServerConfiguration
|
||||
import net.corda.nodeapi.RPCApi
|
||||
import net.corda.nodeapi.RPCKryo
|
||||
import net.corda.testing.*
|
||||
import org.apache.activemq.artemis.api.core.SimpleString
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import rx.Observable
|
||||
import rx.subjects.PublishSubject
|
||||
import rx.subjects.UnicastSubject
|
||||
import java.time.Duration
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
|
||||
class RPCStabilityTests {
|
||||
|
||||
object DummyOps : RPCOps {
|
||||
override val protocolVersion = 0
|
||||
}
|
||||
|
||||
private fun waitUntilNumberOfThreadsStable(executorService: ScheduledExecutorService): Int {
|
||||
val values = ConcurrentLinkedQueue<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 {
|
||||
fun leakObservable(): Observable<Nothing>
|
||||
}
|
||||
@ -42,7 +197,7 @@ class RPCStabilityTests {
|
||||
}
|
||||
}
|
||||
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
|
||||
val N = 200
|
||||
(1..N).toList().parallelStream().forEach {
|
||||
@ -57,6 +212,31 @@ class RPCStabilityTests {
|
||||
}
|
||||
}
|
||||
|
||||
interface ReconnectOps : RPCOps {
|
||||
fun ping(): String
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `client reconnects to rebooted server`() {
|
||||
rpcDriver {
|
||||
val ops = object : ReconnectOps {
|
||||
override val protocolVersion = 0
|
||||
override fun ping() = "pong"
|
||||
}
|
||||
val serverFollower = shutdownManager.follower()
|
||||
val serverPort = startRpcServer<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 {
|
||||
fun subscribe(): Observable<Unit>
|
||||
}
|
||||
@ -86,7 +266,7 @@ class RPCStabilityTests {
|
||||
|
||||
val numberOfClients = 4
|
||||
val clients = Futures.allAsList((1 .. numberOfClients).map {
|
||||
startRandomRpcClient<TrackSubscriberOps>(server.hostAndPort)
|
||||
startRandomRpcClient<TrackSubscriberOps>(server.broker.hostAndPort!!)
|
||||
}).get()
|
||||
|
||||
// Poll until all clients connect
|
||||
@ -131,7 +311,7 @@ class RPCStabilityTests {
|
||||
|
||||
// Construct an RPC session manually so that we can hang in the message handler
|
||||
val myQueue = "${RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX}.test.${random63BitValue()}"
|
||||
val session = startArtemisSession(server.hostAndPort)
|
||||
val session = startArtemisSession(server.broker.hostAndPort!!)
|
||||
session.createTemporaryQueue(myQueue, myQueue)
|
||||
val consumer = session.createConsumer(myQueue, null, -1, -1, false)
|
||||
consumer.setMessageHandler {
|
||||
@ -163,7 +343,7 @@ class RPCStabilityTests {
|
||||
|
||||
fun RPCDriverExposedDSLInterface.pollUntilClientNumber(server: RpcServerHandle, expected: Int) {
|
||||
pollUntilTrue("number of RPC clients to become $expected") {
|
||||
val clientAddresses = server.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
|
||||
val clientAddresses = server.broker.serverControl.addressNames.filter { it.startsWith(RPCApi.RPC_CLIENT_QUEUE_NAME_PREFIX) }
|
||||
clientAddresses.size == expected
|
||||
}.get()
|
||||
}
|
@ -9,10 +9,12 @@ import net.corda.nodeapi.ConnectionDirection
|
||||
import net.corda.nodeapi.config.SSLConfiguration
|
||||
import java.time.Duration
|
||||
|
||||
/** @see RPCClient.RPCConnection */
|
||||
class CordaRPCConnection internal constructor(
|
||||
connection: RPCClient.RPCConnection<CordaRPCOps>
|
||||
) : RPCClient.RPCConnection<CordaRPCOps> by connection
|
||||
|
||||
/** @see RPCClientConfiguration */
|
||||
data class CordaRPCClientConfiguration(
|
||||
val connectionMaxRetryInterval: Duration
|
||||
) {
|
||||
@ -29,6 +31,7 @@ data class CordaRPCClientConfiguration(
|
||||
}
|
||||
}
|
||||
|
||||
/** @see RPCClient */
|
||||
class CordaRPCClient(
|
||||
hostAndPort: HostAndPort,
|
||||
sslConfiguration: SSLConfiguration? = null,
|
||||
|
@ -53,10 +53,12 @@ data class RPCClientConfiguration(
|
||||
val connectionRetryIntervalMultiplier: Double,
|
||||
/** Maximum retry interval */
|
||||
val connectionMaxRetryInterval: Duration,
|
||||
val maxReconnectAttempts: Int,
|
||||
/** Maximum file size */
|
||||
val maxFileSize: Int
|
||||
) {
|
||||
companion object {
|
||||
val unlimitedReconnectAttempts = -1
|
||||
@JvmStatic
|
||||
val default = RPCClientConfiguration(
|
||||
minimumServerProtocolVersion = 0,
|
||||
@ -68,6 +70,7 @@ data class RPCClientConfiguration(
|
||||
connectionRetryInterval = 5.seconds,
|
||||
connectionRetryIntervalMultiplier = 1.5,
|
||||
connectionMaxRetryInterval = 3.minutes,
|
||||
maxReconnectAttempts = unlimitedReconnectAttempts,
|
||||
/** 10 MiB maximum allowed file size for attachments, including message headers. TODO: acquire this value from Network Map when supported. */
|
||||
maxFileSize = 10485760
|
||||
)
|
||||
@ -114,9 +117,9 @@ class RPCClient<I : RPCOps>(
|
||||
*
|
||||
* The [RPCOps] defines what client RPCs are available. If an RPC returns an [Observable] anywhere in the object
|
||||
* graph returned then the server-side observable is transparently forwarded to the client side here.
|
||||
* *You are expected to use it*. The server will begin buffering messages immediately that it will expect you to
|
||||
* drain by subscribing to the returned observer. You can opt-out of this by simply calling the
|
||||
* [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually
|
||||
* *You are expected to use it*. The server will begin sending messages immediately that will be buffered on the
|
||||
* client, you are expected to drain by subscribing to the returned observer. You can opt-out of this by simply
|
||||
* calling the [net.corda.client.rpc.notUsed] method on it. You don't have to explicitly close the observable if you actually
|
||||
* subscribe to it: it will close itself and free up the server-side resources either when the client or JVM itself
|
||||
* is shutdown, or when there are no more subscribers to it. Once all the subscribers to a returned observable are
|
||||
* unsubscribed or the observable completes successfully or with an error, the observable is closed and you can't
|
||||
@ -139,30 +142,37 @@ class RPCClient<I : RPCOps>(
|
||||
retryInterval = rpcConfiguration.connectionRetryInterval.toMillis()
|
||||
retryIntervalMultiplier = rpcConfiguration.connectionRetryIntervalMultiplier
|
||||
maxRetryInterval = rpcConfiguration.connectionMaxRetryInterval.toMillis()
|
||||
reconnectAttempts = rpcConfiguration.maxReconnectAttempts
|
||||
minLargeMessageSize = rpcConfiguration.maxFileSize
|
||||
}
|
||||
|
||||
val proxyHandler = RPCClientProxyHandler(rpcConfiguration, username, password, serverLocator, clientAddress, rpcOpsClass)
|
||||
proxyHandler.start()
|
||||
try {
|
||||
proxyHandler.start()
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val ops = Proxy.newProxyInstance(rpcOpsClass.classLoader, arrayOf(rpcOpsClass), proxyHandler) as I
|
||||
|
||||
val serverProtocolVersion = ops.protocolVersion
|
||||
if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) {
|
||||
throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" +
|
||||
" than the server's supported protocol version ($serverProtocolVersion)")
|
||||
}
|
||||
proxyHandler.setServerProtocolVersion(serverProtocolVersion)
|
||||
|
||||
log.debug("RPC connected, returning proxy")
|
||||
object : RPCConnection<I> {
|
||||
override val proxy = ops
|
||||
override val serverProtocolVersion = serverProtocolVersion
|
||||
override fun close() {
|
||||
proxyHandler.close()
|
||||
serverLocator.close()
|
||||
val serverProtocolVersion = ops.protocolVersion
|
||||
if (serverProtocolVersion < rpcConfiguration.minimumServerProtocolVersion) {
|
||||
throw RPCException("Requested minimum protocol version (${rpcConfiguration.minimumServerProtocolVersion}) is higher" +
|
||||
" than the server's supported protocol version ($serverProtocolVersion)")
|
||||
}
|
||||
proxyHandler.setServerProtocolVersion(serverProtocolVersion)
|
||||
|
||||
log.debug("RPC connected, returning proxy")
|
||||
object : RPCConnection<I> {
|
||||
override val proxy = ops
|
||||
override val serverProtocolVersion = serverProtocolVersion
|
||||
override fun close() {
|
||||
proxyHandler.close()
|
||||
serverLocator.close()
|
||||
}
|
||||
}
|
||||
} catch (exception: Throwable) {
|
||||
proxyHandler.close()
|
||||
serverLocator.close()
|
||||
throw exception
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,16 +25,11 @@ import org.apache.activemq.artemis.api.core.client.ServerLocator
|
||||
import rx.Notification
|
||||
import rx.Observable
|
||||
import rx.subjects.UnicastSubject
|
||||
import sun.reflect.CallerSensitive
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.Method
|
||||
import java.util.*
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledFuture
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.*
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.collections.ArrayList
|
||||
import kotlin.reflect.jvm.javaMethod
|
||||
|
||||
/**
|
||||
@ -81,16 +76,13 @@ class RPCClientProxyHandler(
|
||||
val log = loggerFor<RPCClientProxyHandler>()
|
||||
// Note that this KryoPool is not yet capable of deserialising Observables, it requires Proxy-specific context
|
||||
// to do that. However it may still be used for serialisation of RPC requests and related messages.
|
||||
val kryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
|
||||
val kryoPool: KryoPool = KryoPool.Builder { RPCKryo(RpcClientObservableSerializer) }.build()
|
||||
// To check whether toString() is being invoked
|
||||
val toStringMethod: Method = Object::toString.javaMethod!!
|
||||
}
|
||||
|
||||
// Used for reaping
|
||||
private val reaperExecutor = Executors.newScheduledThreadPool(
|
||||
1,
|
||||
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
|
||||
)
|
||||
private var reaperExecutor: ScheduledExecutorService? = null
|
||||
|
||||
// A sticky pool for running Observable.onNext()s. We need the stickiness to preserve the observation ordering.
|
||||
private val observationExecutorThreadFactory = ThreadFactoryBuilder().setNameFormat("rpc-client-observation-pool-%d").build()
|
||||
@ -109,7 +101,7 @@ class RPCClientProxyHandler(
|
||||
hardReferenceStore = Collections.synchronizedSet(mutableSetOf<Observable<*>>())
|
||||
)
|
||||
// Holds a reference to the scheduled reaper.
|
||||
private lateinit var reaperScheduledFuture: ScheduledFuture<*>
|
||||
private var reaperScheduledFuture: ScheduledFuture<*>? = null
|
||||
// The protocol version of the server, to be initialised to the value of [RPCOps.protocolVersion]
|
||||
private var serverProtocolVersion: Int? = null
|
||||
|
||||
@ -145,7 +137,7 @@ class RPCClientProxyHandler(
|
||||
// TODO We may need to pool these somehow anyway, otherwise if the server sends many big messages in parallel a
|
||||
// single consumer may be starved for flow control credits. Recheck this once Artemis's large message streaming is
|
||||
// integrated properly.
|
||||
private lateinit var sessionAndConsumer: ArtemisConsumer
|
||||
private var sessionAndConsumer: ArtemisConsumer? = null
|
||||
// Pool producers to reduce contention on the client side.
|
||||
private val sessionAndProducerPool = LazyPool(bound = rpcConfiguration.producerPoolBound) {
|
||||
// Note how we create new sessions *and* session factories per producer.
|
||||
@ -162,7 +154,12 @@ class RPCClientProxyHandler(
|
||||
* Start the client. This creates the per-client queue, starts the consumer session and the reaper.
|
||||
*/
|
||||
fun start() {
|
||||
reaperScheduledFuture = reaperExecutor.scheduleAtFixedRate(
|
||||
lifeCycle.requireState(State.UNSTARTED)
|
||||
reaperExecutor = Executors.newScheduledThreadPool(
|
||||
1,
|
||||
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
|
||||
)
|
||||
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
|
||||
this::reapObservables,
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
rpcConfiguration.reapInterval.toMillis(),
|
||||
@ -187,7 +184,7 @@ class RPCClientProxyHandler(
|
||||
if (method == toStringMethod) {
|
||||
return "Client RPC proxy for $rpcOpsClass"
|
||||
}
|
||||
if (sessionAndConsumer.session.isClosed) {
|
||||
if (sessionAndConsumer!!.session.isClosed) {
|
||||
throw RPCException("RPC Proxy is closed")
|
||||
}
|
||||
val rpcId = RPCApi.RpcRequestId(random63BitValue())
|
||||
@ -211,6 +208,12 @@ class RPCClientProxyHandler(
|
||||
it.session.commit()
|
||||
}
|
||||
return replyFuture.getOrThrow()
|
||||
} catch (e: RuntimeException) {
|
||||
// Already an unchecked exception, so just rethrow it
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
// This must be a checked exception, so wrap it
|
||||
throw RPCException(e.message ?: "", e)
|
||||
} finally {
|
||||
callSiteMap?.remove(rpcId.toLong)
|
||||
}
|
||||
@ -268,24 +271,19 @@ class RPCClientProxyHandler(
|
||||
* Closes the RPC proxy. Reaps all observables, shuts down the reaper, closes all sessions and executors.
|
||||
*/
|
||||
fun close() {
|
||||
sessionAndConsumer.consumer.close()
|
||||
sessionAndConsumer.session.close()
|
||||
sessionAndConsumer.sessionFactory.close()
|
||||
reaperScheduledFuture.cancel(false)
|
||||
sessionAndConsumer?.sessionFactory?.close()
|
||||
reaperScheduledFuture?.cancel(false)
|
||||
observableContext.observableMap.invalidateAll()
|
||||
reapObservables()
|
||||
reaperExecutor.shutdownNow()
|
||||
reaperExecutor?.shutdownNow()
|
||||
sessionAndProducerPool.close().forEach {
|
||||
it.producer.close()
|
||||
it.session.close()
|
||||
it.sessionFactory.close()
|
||||
}
|
||||
// Note the ordering is important, we shut down the consumer *before* the observation executor, otherwise we may
|
||||
// leak borrowed executors.
|
||||
val observationExecutors = observationExecutorPool.close()
|
||||
observationExecutors.forEach { it.shutdownNow() }
|
||||
observationExecutors.forEach { it.awaitTermination(100, TimeUnit.MILLISECONDS) }
|
||||
lifeCycle.transition(State.STARTED, State.FINISHED)
|
||||
lifeCycle.justTransition(State.FINISHED)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
@ -47,8 +47,8 @@ open class AbstractRPCTest {
|
||||
}.get()
|
||||
RPCTestMode.Netty ->
|
||||
startRpcServer(ops = ops, rpcUser = rpcUser, configuration = serverConfiguration).flatMap { server ->
|
||||
startRpcClient<I>(server.hostAndPort, rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startArtemisSession(server.hostAndPort, rpcUser.username, rpcUser.password) })
|
||||
startRpcClient<I>(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password, clientConfiguration).map {
|
||||
TestProxy(it, { startArtemisSession(server.broker.hostAndPort!!, rpcUser.username, rpcUser.password) })
|
||||
}
|
||||
}.get()
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
gradlePluginsVersion=0.12.0
|
||||
gradlePluginsVersion=0.12.1
|
||||
kotlinVersion=1.1.2
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.56
|
||||
|
15
cordform-common/build.gradle
Normal file
15
cordform-common/build.gradle
Normal 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"
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ dependencies {
|
||||
compile "com.fasterxml.jackson.core:jackson-databind:${jackson_version}"
|
||||
|
||||
// Java ed25519 implementation. See https://github.com/str4d/ed25519-java/
|
||||
compile 'net.i2p.crypto:eddsa:0.2.0'
|
||||
compile "net.i2p.crypto:eddsa:$eddsa_version"
|
||||
|
||||
// Bouncy castle support needed for X509 certificate manipulation
|
||||
compile "org.bouncycastle:bcprov-jdk15on:${bouncycastle_version}"
|
||||
|
@ -9,6 +9,7 @@ import com.google.common.util.concurrent.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.crypto.sha256
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
@ -32,13 +33,7 @@ import java.util.zip.Deflater
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
import java.util.zip.ZipOutputStream
|
||||
import kotlin.collections.Iterable
|
||||
import kotlin.collections.LinkedHashMap
|
||||
import kotlin.collections.List
|
||||
import kotlin.collections.filter
|
||||
import kotlin.collections.firstOrNull
|
||||
import kotlin.collections.fold
|
||||
import kotlin.collections.forEach
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
@ -113,8 +108,17 @@ infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T
|
||||
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.
|
||||
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!!) }
|
||||
|
||||
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. */
|
||||
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
|
||||
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. */
|
||||
operator fun Path.div(other: String): Path = resolve(other)
|
||||
operator fun Path.div(other: String) = resolve(other)
|
||||
operator fun String.div(other: String) = Paths.get(this) / other
|
||||
|
||||
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
|
||||
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
|
||||
@ -271,7 +276,7 @@ class ThreadBox<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.
|
||||
*/
|
||||
@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
|
||||
|
@ -56,6 +56,7 @@ infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantit
|
||||
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
object Requirements {
|
||||
/** Throws [IllegalArgumentException] if the given expression evaluates to false. */
|
||||
@Suppress("NOTHING_TO_INLINE") // Inlining this takes it out of our committed ABI.
|
||||
infix inline fun String.using(expr: Boolean) {
|
||||
if (!expr) throw IllegalArgumentException("Failed requirement: $this")
|
||||
@ -93,13 +94,14 @@ inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>
|
||||
filter { if (parties == null) true else it.signingParties.containsAll(parties) }.
|
||||
map { AuthenticatedObject(it.signers, it.signingParties, it.value as T) }
|
||||
|
||||
/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */
|
||||
inline fun <reified T : CommandData> Collection<AuthenticatedObject<CommandData>>.requireSingleCommand() = try {
|
||||
select<T>().single()
|
||||
} catch (e: NoSuchElementException) {
|
||||
throw IllegalStateException("Required ${T::class.qualifiedName} command") // Better error message.
|
||||
}
|
||||
|
||||
// For Java
|
||||
/** Ensures that a transaction has only one command that is of the given type, otherwise throws an exception. */
|
||||
fun <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()
|
||||
|
||||
@ -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
|
||||
// see a signature from each of those keys. The actual signatures have been verified against the transaction
|
||||
// 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 keysThatSigned = command.signers.toSet()
|
||||
requireThat {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
|
||||
@ -14,12 +14,12 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
val magicNumber: Int
|
||||
}
|
||||
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: PublicKey) : OwnableState, State {
|
||||
data class SingleOwnerState(override val magicNumber: Int = 0, override val owner: AbstractParty) : OwnableState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
/**
|
||||
@ -28,9 +28,9 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
* in a different field, however this is a good example of a contract with multiple states.
|
||||
*/
|
||||
data class MultiOwnerState(override val magicNumber: Int = 0,
|
||||
val owners: List<PublicKey>) : ContractState, State {
|
||||
val owners: List<AbstractParty>) : ContractState, State {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey> get() = owners
|
||||
override val participants: List<AbstractParty> get() = owners
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
@ -47,22 +47,22 @@ data class DummyContract(override val legalContractReference: SecureHash = Secur
|
||||
fun generateInitial(magicNumber: Int, notary: Party, owner: PartyAndReference, vararg otherOwners: PartyAndReference): TransactionBuilder {
|
||||
val owners = listOf(owner) + otherOwners
|
||||
return if (owners.size == 1) {
|
||||
val state = SingleOwnerState(magicNumber, owners.first().party.owningKey)
|
||||
val state = SingleOwnerState(magicNumber, owners.first().party)
|
||||
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.first().party.owningKey))
|
||||
} else {
|
||||
val state = MultiOwnerState(magicNumber, owners.map { it.party.owningKey })
|
||||
val state = MultiOwnerState(magicNumber, owners.map { it.party })
|
||||
TransactionType.General.Builder(notary = notary).withItems(state, Command(Commands.Create(), owners.map { it.party.owningKey }))
|
||||
}
|
||||
}
|
||||
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: PublicKey) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: PublicKey): TransactionBuilder {
|
||||
fun move(prior: StateAndRef<DummyContract.SingleOwnerState>, newOwner: AbstractParty) = move(listOf(prior), newOwner)
|
||||
fun move(priors: List<StateAndRef<DummyContract.SingleOwnerState>>, newOwner: AbstractParty): TransactionBuilder {
|
||||
require(priors.isNotEmpty())
|
||||
val priorState = priors[0].state.data
|
||||
val (cmd, state) = priorState.withNewOwner(newOwner)
|
||||
return TransactionType.General.Builder(notary = priors[0].state.notary).withItems(
|
||||
/* INPUTS */ *priors.toTypedArray(),
|
||||
/* COMMAND */ Command(cmd, priorState.owner),
|
||||
/* COMMAND */ Command(cmd, priorState.owner.owningKey),
|
||||
/* OUTPUT */ state
|
||||
)
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
import java.security.PublicKey
|
||||
|
||||
// The dummy contract doesn't do anything useful. It exists for testing purposes.
|
||||
val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
@ -15,9 +15,9 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||
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 participants: List<PublicKey> = owners
|
||||
override val participants: List<AbstractParty> = owners
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
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()
|
||||
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 {
|
||||
states.forEach {
|
||||
addInputState(it)
|
||||
addOutputState(upgrade(it.state.data))
|
||||
addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.toList())
|
||||
addCommand(UpgradeCommand(DUMMY_V2_PROGRAM_ID.javaClass), signees.map { it.owningKey }.toList())
|
||||
}
|
||||
}.toWireTransaction(), signees)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import java.security.PublicKey
|
||||
import net.corda.core.identity.AbstractParty
|
||||
|
||||
/**
|
||||
* Dummy state for use in testing. Not part of any contract, not even the [DummyContract].
|
||||
*/
|
||||
data class DummyState(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = DUMMY_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
}
|
||||
|
@ -1,12 +1,8 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.loggerFor
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
|
||||
class InsufficientBalanceException(val amountMissing: Amount<*>) : FlowException("Insufficient balance, missing $amountMissing")
|
||||
|
||||
@ -32,9 +28,9 @@ interface FungibleAsset<T : Any> : OwnableState {
|
||||
*/
|
||||
val exitKeys: Collection<PublicKey>
|
||||
/** 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
|
||||
interface Commands : CommandData {
|
||||
|
@ -4,6 +4,7 @@ import net.corda.core.contracts.clauses.Clause
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.ServiceType
|
||||
@ -114,7 +115,7 @@ interface ContractState {
|
||||
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants
|
||||
* list should just contain the owner.
|
||||
*/
|
||||
val participants: List<PublicKey>
|
||||
val participants: List<AbstractParty>
|
||||
}
|
||||
|
||||
/**
|
||||
@ -174,10 +175,10 @@ fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, to
|
||||
*/
|
||||
interface OwnableState : ContractState {
|
||||
/** There must be a MoveCommand signed by this key to claim the amount */
|
||||
val owner: PublicKey
|
||||
val owner: AbstractParty
|
||||
|
||||
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
|
||||
fun withNewOwner(newOwner: PublicKey): Pair<CommandData, OwnableState>
|
||||
fun withNewOwner(newOwner: AbstractParty): Pair<CommandData, OwnableState>
|
||||
}
|
||||
|
||||
/** Something which is scheduled to happen at a point in time */
|
||||
@ -280,7 +281,7 @@ interface DealState : LinearState {
|
||||
* separate process exchange certificates to ascertain identities. Thus decoupling identities from
|
||||
* [ContractState]s.
|
||||
* */
|
||||
val parties: List<AnonymousParty>
|
||||
val parties: List<AbstractParty>
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
|
||||
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
|
||||
|
||||
data class PartyAndReference(val party: AbstractParty, val reference: OpaqueBytes) {
|
||||
override fun toString() = "$party$reference"
|
||||
}
|
||||
|
||||
@ -411,7 +410,12 @@ data class AuthenticatedObject<out T : Any>(
|
||||
* between (after, before).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Timestamp(val after: Instant?, val before: Instant?) {
|
||||
data class Timestamp(
|
||||
/** The time at which this transaction is said to have occurred is after this moment */
|
||||
val after: Instant?,
|
||||
/** The time at which this transaction is said to have occurred is before this moment */
|
||||
val before: Instant?
|
||||
) {
|
||||
init {
|
||||
if (after == null && before == null)
|
||||
throw IllegalArgumentException("At least one of before/after must be specified")
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
@ -60,7 +61,7 @@ sealed class TransactionType {
|
||||
abstract fun verifyTransaction(tx: LedgerTransaction)
|
||||
|
||||
/** A general transaction type where transaction validity is determined by custom contract code */
|
||||
object General : TransactionType() {
|
||||
object General : TransactionType(), DeserializeAsKotlinObjectDef {
|
||||
/** Just uses the default [TransactionBuilder] with no special logic */
|
||||
class Builder(notary: Party?) : TransactionBuilder(General, notary)
|
||||
|
||||
@ -140,14 +141,14 @@ sealed class TransactionType {
|
||||
* A special transaction type for reassigning a notary for a state. Validation does not involve running
|
||||
* any contract code, it just checks that the states are unmodified apart from the notary field.
|
||||
*/
|
||||
object NotaryChange : TransactionType() {
|
||||
object NotaryChange : TransactionType(), DeserializeAsKotlinObjectDef {
|
||||
/**
|
||||
* A transaction builder that automatically sets the transaction type to [NotaryChange]
|
||||
* and adds the list of participants to the signers set for every input state.
|
||||
*/
|
||||
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
|
||||
override fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||
signers.addAll(stateAndRef.state.data.participants)
|
||||
signers.addAll(stateAndRef.state.data.participants.map { it.owningKey })
|
||||
super.addInputState(stateAndRef)
|
||||
}
|
||||
}
|
||||
@ -170,6 +171,6 @@ sealed class TransactionType {
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.toSet()
|
||||
override fun getRequiredSigners(tx: LedgerTransaction) = tx.inputs.flatMap { it.state.data.participants }.map { it.owningKey }.toSet()
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.serialization.deserialize
|
||||
import org.bouncycastle.asn1.ASN1ObjectIdentifier
|
||||
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.security.*
|
||||
import java.security.spec.AlgorithmParameterSpec
|
||||
@ -10,7 +12,12 @@ import java.security.spec.AlgorithmParameterSpec
|
||||
*/
|
||||
class CompositeSignature : Signature(ALGORITHM) {
|
||||
companion object {
|
||||
val ALGORITHM = "X-Corda-CompositeSig"
|
||||
val ALGORITHM = "2.25.30086077608615255153862931087626791003"
|
||||
// UUID-based OID
|
||||
// TODO: Register for an OID space and issue our own shorter OID
|
||||
val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM))
|
||||
|
||||
fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
|
||||
}
|
||||
|
||||
private var signatureState: State? = null
|
||||
|
@ -21,13 +21,20 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
|
||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateKey
|
||||
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
|
||||
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
|
||||
import org.bouncycastle.jce.ECNamedCurveTable
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||
import org.bouncycastle.pkcs.PKCS10CertificationRequest
|
||||
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
|
||||
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
||||
import sun.security.pkcs.PKCS8Key
|
||||
import sun.security.util.DerValue
|
||||
import sun.security.x509.X509Key
|
||||
import java.math.BigInteger
|
||||
import java.security.*
|
||||
import java.security.KeyFactory
|
||||
@ -140,6 +147,10 @@ object Crypto {
|
||||
SPHINCS256_SHA256
|
||||
).associateBy { it.schemeCodeName }
|
||||
|
||||
// We need to group signature schemes per algorithm, so to quickly identify them during decoding.
|
||||
// Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256.
|
||||
private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName }
|
||||
|
||||
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
|
||||
// that could cause unexpected and suspicious behaviour.
|
||||
// i.e. if someone removes a Provider and then he/she adds a new one with the same name.
|
||||
@ -167,37 +178,20 @@ object Crypto {
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||
*/
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: $schemeCodeName")
|
||||
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
|
||||
|
||||
/**
|
||||
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
|
||||
* This function is usually called when requiring to verify signatures and the signing schemes must be defined.
|
||||
* Note that only the Corda platform standard schemes are supported (see [Crypto]).
|
||||
* Note that we always need to add an additional if-else statement when there are signature schemes
|
||||
* with the same algorithmName, but with different parameters (e.g. now there are two ECDSA schemes, each using its own curve).
|
||||
* For the supported signature schemes see [Crypto].
|
||||
* @param key either private or public.
|
||||
* @return a currently supported SignatureScheme.
|
||||
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||
*/
|
||||
fun findSignatureScheme(key: Key): SignatureScheme {
|
||||
for (sig in supportedSignatureSchemes.values) {
|
||||
var algorithm = key.algorithm
|
||||
if (algorithm == "EC") algorithm = "ECDSA" // required to read ECC keys from Keystore, because encoding may change algorithm name from ECDSA to EC.
|
||||
if (algorithm == "SPHINCS-256") algorithm = "SPHINCS256" // because encoding may change algorithm name from SPHINCS256 to SPHINCS-256.
|
||||
if (algorithm == sig.algorithmName) {
|
||||
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters.
|
||||
if (algorithm == "EdDSA") {
|
||||
if ((key is EdDSAPublicKey && publicKeyOnCurve(sig, key)) || (key is EdDSAPrivateKey && key.params == sig.algSpec)) {
|
||||
return sig
|
||||
} else break // use continue if in the future we support more than one Edwards curves.
|
||||
} else if (algorithm == "ECDSA") {
|
||||
if ((key is BCECPublicKey && publicKeyOnCurve(sig, key)) || (key is BCECPrivateKey && key.parameters == sig.algSpec)) {
|
||||
return sig
|
||||
} else continue
|
||||
} else return sig // it's either RSA_SHA256 or SPHINCS-256.
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for the key: ${key.encoded.toBase58()}")
|
||||
val algorithm = matchingAlgorithmName(key.algorithm)
|
||||
algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it }
|
||||
throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format")
|
||||
}
|
||||
|
||||
/**
|
||||
@ -209,11 +203,16 @@ object Crypto {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
|
||||
val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm)
|
||||
// There are cases where the same key algorithm is applied to different signature schemes.
|
||||
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
|
||||
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
|
||||
// to generate the appropriate key.
|
||||
for (signatureScheme in algorithmGroups[algorithm]!!) {
|
||||
try {
|
||||
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
// ignore it - only used to bypass the scheme that causes an exception.
|
||||
// ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.")
|
||||
@ -240,6 +239,8 @@ object Crypto {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
@ -256,11 +257,16 @@ object Crypto {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
|
||||
for ((_, _, _, providerName, algorithmName) in supportedSignatureSchemes.values) {
|
||||
val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm)
|
||||
// There are cases where the same key algorithm is applied to different signature schemes.
|
||||
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
|
||||
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
|
||||
// to generate the appropriate key.
|
||||
for (signatureScheme in algorithmGroups[algorithm]!!) {
|
||||
try {
|
||||
return KeyFactory.getInstance(algorithmName, providerMap[providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
// ignore it - only used to bypass the scheme that causes an exception.
|
||||
// ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
|
||||
}
|
||||
}
|
||||
throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.")
|
||||
@ -271,7 +277,7 @@ object Crypto {
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported.
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@ -283,12 +289,14 @@ object Crypto {
|
||||
* This should be used when the type key is known, e.g. during Kryo deserialisation or with key caches or key managers.
|
||||
* @param signatureScheme a signature scheme (e.g. ECDSA_SECP256K1_SHA256).
|
||||
* @param encodedKey an X509 encoded public key.
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported
|
||||
* @throws IllegalArgumentException if the requested scheme is not supported.
|
||||
* @throws InvalidKeySpecException if the given key specification
|
||||
* is inappropriate for this key factory to produce a public key.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
try {
|
||||
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
|
||||
} catch (ikse: InvalidKeySpecException) {
|
||||
@ -334,7 +342,7 @@ object Crypto {
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
|
||||
@ -414,7 +422,7 @@ object Crypto {
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
|
||||
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
|
||||
@ -440,7 +448,7 @@ object Crypto {
|
||||
*/
|
||||
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||
fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean {
|
||||
if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.encoded.toBase58()} does not match the input clearData: ${publicKey.encoded.toBase58()}")
|
||||
if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.toStringShort()} does not match")
|
||||
return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes())
|
||||
}
|
||||
|
||||
@ -477,7 +485,7 @@ object Crypto {
|
||||
*/
|
||||
@Throws(SignatureException::class, IllegalArgumentException::class)
|
||||
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
|
||||
signature.initVerify(publicKey)
|
||||
@ -505,7 +513,7 @@ object Crypto {
|
||||
@Throws(IllegalArgumentException::class)
|
||||
@JvmOverloads
|
||||
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
|
||||
if (!supportedSignatureSchemes.containsKey(signatureScheme.schemeCodeName))
|
||||
if (!isSupportedSignatureScheme(signatureScheme))
|
||||
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
|
||||
if (signatureScheme.algSpec != null)
|
||||
@ -547,20 +555,16 @@ object Crypto {
|
||||
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
|
||||
}
|
||||
|
||||
/** Check if the requested signature scheme is supported by the system. */
|
||||
fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
|
||||
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = signatureScheme.schemeCodeName in supportedSignatureSchemes
|
||||
|
||||
/**
|
||||
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key
|
||||
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key.
|
||||
*/
|
||||
fun createCertificate(issuer: X500Name, issuerKeyPair: KeyPair,
|
||||
subject: X500Name, subjectPublicKey: PublicKey,
|
||||
keyUsage: KeyUsage, purposes: List<KeyPurposeId>,
|
||||
signatureScheme: SignatureScheme, validityWindow: Pair<Date, Date>,
|
||||
validityWindow: Pair<Date, Date>,
|
||||
pathLength: Int? = null, subjectAlternativeName: List<GeneralName>? = null): X509Certificate {
|
||||
|
||||
val signatureScheme = findSignatureScheme(issuerKeyPair.private)
|
||||
val provider = providerMap[signatureScheme.providerName]
|
||||
val serial = BigInteger.valueOf(random63BitValue())
|
||||
val keyPurposes = DERSequence(ASN1EncodableVector().apply { purposes.forEach { add(it) } })
|
||||
@ -598,9 +602,9 @@ object Crypto {
|
||||
* Check if a point's coordinates are on the expected curve to avoid certain types of ECC attacks.
|
||||
* Point-at-infinity is not permitted as well.
|
||||
* @see <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
|
||||
* we're doing it out of an abundance of caution and specifically to proactively protect developers
|
||||
* we are doing it out of an abundance of caution and specifically to proactively protect developers
|
||||
* against using these points as part of a DH key agreement or for use cases as yet unimagined.
|
||||
* This method currently applies to BouncyCastle's ECDSA (both R1 and K1 curves) and I2P's EdDSA (ed25519 curve).
|
||||
* @param publicKey a [PublicKey], usually used to validate a signer's public key in on the Curve.
|
||||
@ -622,4 +626,69 @@ object Crypto {
|
||||
// return true if EdDSA publicKey is point at infinity.
|
||||
// For EdDSA a custom function is required as it is not supported by the I2P implementation.
|
||||
private fun isEdDSAPointAtInfinity(publicKey: EdDSAPublicKey) = publicKey.a.toP3() == (EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec).curve.getZero(GroupElement.Representation.P3)
|
||||
|
||||
/** Check if the requested [SignatureScheme] is supported by the system. */
|
||||
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
|
||||
|
||||
// map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names.
|
||||
private fun matchingAlgorithmName(algorithm: String): String {
|
||||
return when (algorithm) {
|
||||
"EC" -> "ECDSA"
|
||||
"SPHINCS-256" -> "SPHINCS256"
|
||||
"1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256.
|
||||
else -> algorithm
|
||||
}
|
||||
}
|
||||
|
||||
// validate a key, by checking its algorithmic params.
|
||||
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
|
||||
return when (key) {
|
||||
is PublicKey -> validatePublicKey(signatureScheme, key)
|
||||
is PrivateKey -> validatePrivateKey(signatureScheme, key)
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// check if a public key satisfies algorithm specs (for ECC: key should lie on the curve and not being point-at-infinity).
|
||||
private fun validatePublicKey(signatureScheme: SignatureScheme, key: PublicKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPublicKey, is EdDSAPublicKey -> return publicKeyOnCurve(signatureScheme, key)
|
||||
is BCRSAPublicKey, is BCSphincs256PublicKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
// check if a private key satisfies algorithm specs.
|
||||
private fun validatePrivateKey(signatureScheme: SignatureScheme, key: PrivateKey): Boolean {
|
||||
when (key) {
|
||||
is BCECPrivateKey -> return key.parameters == signatureScheme.algSpec
|
||||
is EdDSAPrivateKey -> return key.params == signatureScheme.algSpec
|
||||
is BCRSAPrivateKey, is BCSphincs256PrivateKey -> return true // TODO: Check if non-ECC keys satisfy params (i.e. approved/valid RSA modulus size).
|
||||
else -> throw IllegalArgumentException("Unsupported key type: ${key::class}")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.
|
||||
* @param key a public key.
|
||||
* @return a supported implementation of the input public key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPublicKey(key: PublicKey): PublicKey {
|
||||
return Crypto.decodePublicKey(key.encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a private key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
|
||||
* This method is usually required to retrieve keys from JKS keystores that by default return SUN implementations.
|
||||
* @param key a private key.
|
||||
* @return a supported implementation of the input private key.
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun toSupportedPrivateKey(key: PrivateKey): PrivateKey {
|
||||
return Crypto.decodePrivateKey(key.encoded)
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,11 @@
|
||||
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import java.math.BigInteger
|
||||
import net.corda.core.utilities.SgxSupport
|
||||
import java.security.*
|
||||
@ -19,6 +20,8 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun toString() = "NULL_KEY"
|
||||
}
|
||||
|
||||
val NULL_PARTY = AnonymousParty(NullPublicKey)
|
||||
|
||||
// TODO: Clean up this duplication between Null and Dummy public key
|
||||
@CordaSerializable
|
||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
@ -69,11 +72,9 @@ fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.byte
|
||||
// implementation of CompositeSignature.
|
||||
@Throws(InvalidKeyException::class)
|
||||
fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
|
||||
// Quick workaround when we have CompositeKey as Party owningKey.
|
||||
if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
|
||||
val sig = sign(bytesToSign)
|
||||
val sigKey = when (party.owningKey) { // Quick workaround when we have CompositeKey as Party owningKey.
|
||||
is CompositeKey -> throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
|
||||
else -> party.owningKey
|
||||
}
|
||||
return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
|
||||
}
|
||||
|
||||
|
@ -16,10 +16,10 @@ object KeyStoreUtilities {
|
||||
|
||||
/**
|
||||
* Helper method to either open an existing keystore for modification, or create a new blank keystore.
|
||||
* @param keyStoreFilePath location of KeyStore file
|
||||
* @param keyStoreFilePath location of KeyStore file.
|
||||
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
|
||||
* but for SSL purposes this is recommended.
|
||||
* @return returns the KeyStore opened/created
|
||||
* @return returns the KeyStore opened/created.
|
||||
*/
|
||||
fun loadOrCreateKeyStore(keyStoreFilePath: Path, storePassword: String): KeyStore {
|
||||
val pass = storePassword.toCharArray()
|
||||
@ -34,11 +34,11 @@ object KeyStoreUtilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to open an existing keystore for modification/read
|
||||
* @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException
|
||||
* Helper method to open an existing keystore for modification/read.
|
||||
* @param keyStoreFilePath location of KeyStore file which must exist, or this will throw FileNotFoundException.
|
||||
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
|
||||
* but for SSL purposes this is recommended.
|
||||
* @return returns the KeyStore opened
|
||||
* @return returns the KeyStore opened.
|
||||
* @throws IOException if there was an error reading the key store from the file.
|
||||
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
|
||||
*/
|
||||
@ -48,11 +48,11 @@ object KeyStoreUtilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to open an existing keystore for modification/read
|
||||
* @param input stream containing a KeyStore e.g. loaded from a resource file
|
||||
* Helper method to open an existing keystore for modification/read.
|
||||
* @param input stream containing a KeyStore e.g. loaded from a resource file.
|
||||
* @param storePassword password to open the store. This does not have to be the same password as any keys stored,
|
||||
* but for SSL purposes this is recommended.
|
||||
* @return returns the KeyStore opened
|
||||
* @return returns the KeyStore opened.
|
||||
* @throws IOException if there was an error reading the key store from the stream.
|
||||
* @throws KeyStoreException if the password is incorrect or the key store is damaged.
|
||||
*/
|
||||
@ -68,12 +68,12 @@ object KeyStoreUtilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper extension method to add, or overwrite any key data in store
|
||||
* @param alias name to record the private key and certificate chain under
|
||||
* @param key cryptographic key to store
|
||||
* Helper extension method to add, or overwrite any key data in store.
|
||||
* @param alias name to record the private key and certificate chain under.
|
||||
* @param key cryptographic key to store.
|
||||
* @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored,
|
||||
* but for SSL purposes this is recommended.
|
||||
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert
|
||||
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert.
|
||||
*/
|
||||
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: Array<Certificate>) {
|
||||
if (containsAlias(alias)) {
|
||||
@ -83,9 +83,9 @@ fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper extension method to add, or overwrite any public certificate data in store
|
||||
* @param alias name to record the public certificate under
|
||||
* @param cert certificate to store
|
||||
* Helper extension method to add, or overwrite any public certificate data in store.
|
||||
* @param alias name to record the public certificate under.
|
||||
* @param cert certificate to store.
|
||||
*/
|
||||
fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
||||
if (containsAlias(alias)) {
|
||||
@ -96,8 +96,8 @@ fun KeyStore.addOrReplaceCertificate(alias: String, cert: Certificate) {
|
||||
|
||||
|
||||
/**
|
||||
* Helper method save KeyStore to storage
|
||||
* @param keyStoreFilePath the file location to save to
|
||||
* Helper method save KeyStore to storage.
|
||||
* @param keyStoreFilePath the file location to save to.
|
||||
* @param storePassword password to access the store in future. This does not have to be the same password as any keys stored,
|
||||
* but for SSL purposes this is recommended.
|
||||
*/
|
||||
@ -108,29 +108,47 @@ fun KeyStore.store(out: OutputStream, password: String) = store(out, password.to
|
||||
|
||||
/**
|
||||
* Extract public and private keys from a KeyStore file assuming storage alias is known.
|
||||
* @param keyPassword Password to unlock the private key entries
|
||||
* @param alias The name to lookup the Key and Certificate chain from
|
||||
* @return The KeyPair found in the KeyStore under the specified alias
|
||||
* @param alias The name to lookup the Key and Certificate chain from.
|
||||
* @param keyPassword Password to unlock the private key entries.
|
||||
* @return The KeyPair found in the KeyStore under the specified alias.
|
||||
*/
|
||||
fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKey(alias, keyPassword).keyPair
|
||||
fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertificateAndKeyPair(alias, keyPassword).keyPair
|
||||
|
||||
/**
|
||||
* Helper method to load a Certificate and KeyPair from their KeyStore.
|
||||
* The access details should match those of the createCAKeyStoreAndTrustStore call used to manufacture the keys.
|
||||
* @param keyPassword The password for the PrivateKey (not the store access password)
|
||||
* @param alias The name to search for the data. Typically if generated with the methods here this will be one of
|
||||
* CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above
|
||||
* CERT_PRIVATE_KEY_ALIAS, ROOT_CA_CERT_PRIVATE_KEY_ALIAS, INTERMEDIATE_CA_PRIVATE_KEY_ALIAS defined above.
|
||||
* @param keyPassword The password for the PrivateKey (not the store access password).
|
||||
*/
|
||||
fun KeyStore.getCertificateAndKey(alias: String, keyPassword: String): CertificateAndKey {
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val key = getKey(alias, keyPass) as PrivateKey
|
||||
fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair {
|
||||
val cert = getCertificate(alias) as X509Certificate
|
||||
return CertificateAndKey(cert, KeyPair(cert.publicKey, key))
|
||||
return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract public X509 certificate from a KeyStore file assuming storage alias is know
|
||||
* @param alias The name to lookup the Key and Certificate chain from
|
||||
* @return The X509Certificate found in the KeyStore under the specified alias
|
||||
* Extract public X509 certificate from a KeyStore file assuming storage alias is known.
|
||||
* @param alias The name to lookup the Key and Certificate chain from.
|
||||
* @return The X509Certificate found in the KeyStore under the specified alias.
|
||||
*/
|
||||
fun KeyStore.getX509Certificate(alias: String): X509Certificate = getCertificate(alias) as X509Certificate
|
||||
|
||||
/**
|
||||
* Extract a private key from a KeyStore file assuming storage alias is known.
|
||||
* By default, a JKS keystore returns PrivateKey implementations supported by the SUN provider.
|
||||
* For instance, if one imports a BouncyCastle ECC key, JKS will return a SUN ECC key implementation on getKey.
|
||||
* To convert to a supported implementation, an encode->decode method is applied to the keystore's returned object.
|
||||
* @param alias The name to lookup the Key.
|
||||
* @param keyPassword Password to unlock the private key entries.
|
||||
* @return the requested private key in supported type.
|
||||
* @throws KeyStoreException if the keystore has not been initialized.
|
||||
* @throws NoSuchAlgorithmException if the algorithm for recovering the key cannot be found (not supported from the Keystore provider).
|
||||
* @throws UnrecoverableKeyException if the key cannot be recovered (e.g., the given password is wrong).
|
||||
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||
* is inappropriate for a supported key factory to produce a private key.
|
||||
*/
|
||||
fun KeyStore.getSupportedKey(alias: String, keyPassword: String): PrivateKey {
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val key = getKey(alias, keyPass) as PrivateKey
|
||||
return Crypto.toSupportedPrivateKey(key)
|
||||
}
|
||||
|
@ -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)
|
@ -17,11 +17,12 @@ import java.io.FileWriter
|
||||
import java.io.InputStream
|
||||
import java.net.InetAddress
|
||||
import java.nio.file.Path
|
||||
import java.security.InvalidAlgorithmParameterException
|
||||
import java.security.KeyPair
|
||||
import java.security.KeyStore
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.cert.*
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
import java.util.*
|
||||
@ -42,29 +43,43 @@ object X509Utilities {
|
||||
private val CA_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage)
|
||||
private val CLIENT_KEY_PURPOSES = listOf(KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth)
|
||||
|
||||
private val DEFAULT_VALIDITY_WINDOW = Pair(0, 365 * 10)
|
||||
private val DEFAULT_VALIDITY_WINDOW = Pair(Duration.ofMillis(0), Duration.ofDays(365 * 10))
|
||||
|
||||
/**
|
||||
* Helper method to get a notBefore and notAfter pair from current day bounded by parent certificate validity range
|
||||
* @param daysBefore number of days to roll back returned start date relative to current date
|
||||
* @param daysAfter number of days to roll forward returned end date relative to current date
|
||||
* @param parentNotBefore if provided is used to lower bound the date interval returned
|
||||
* @param parentNotAfter if provided is used to upper bound the date interval returned
|
||||
* Note we use Date rather than LocalDate as the consuming java.security and BouncyCastle certificate apis all use Date
|
||||
* Thus we avoid too many round trip conversions.
|
||||
* Helper function to return the latest out of an instant and an optional date.
|
||||
*/
|
||||
private fun getCertificateValidityWindow(daysBefore: Int, daysAfter: Int, parentNotBefore: Date? = null, parentNotAfter: Date? = null): Pair<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 notBefore = Date.from(startOfDayUTC.minus(daysBefore.toLong(), ChronoUnit.DAYS)).let { notBefore ->
|
||||
if (parentNotBefore != null && parentNotBefore.after(notBefore)) parentNotBefore else notBefore
|
||||
}
|
||||
val notAfter = Date.from(startOfDayUTC.plus(daysAfter.toLong(), ChronoUnit.DAYS)).let { notAfter ->
|
||||
if (parentNotAfter != null && parentNotAfter.after(notAfter)) parentNotAfter else notAfter
|
||||
}
|
||||
val notBefore = max(startOfDayUTC - before, parent?.notBefore)
|
||||
val notAfter = min(startOfDayUTC + after, parent?.notAfter)
|
||||
return Pair(notBefore, notAfter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a bogus X509 for dev purposes.
|
||||
* Return a bogus X509 for dev purposes. Use [getX509Name] for something more real.
|
||||
*/
|
||||
@Deprecated("Full legal names should be specified in all configurations")
|
||||
fun getDevX509Name(commonName: String): X500Name {
|
||||
@ -96,71 +111,106 @@ object X509Utilities {
|
||||
|
||||
/*
|
||||
* Create a de novo root self-signed X509 v3 CA cert and [KeyPair].
|
||||
* @param subject the cert Subject will be populated with the domain string
|
||||
* @param subject the cert Subject will be populated with the domain string.
|
||||
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
|
||||
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
|
||||
* @return A data class is returned containing the new root CA Cert and its [KeyPair] for signing downstream certificates.
|
||||
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates
|
||||
* Note the generated certificate tree is capped at max depth of 2 to be in line with commercially available certificates.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair<Int, Int> = DEFAULT_VALIDITY_WINDOW): CertificateAndKey {
|
||||
val keyPair = generateKeyPair(signatureScheme)
|
||||
fun createSelfSignedCACert(subject: X500Name,
|
||||
keyPair: KeyPair,
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair {
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
|
||||
val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 2)
|
||||
return CertificateAndKey(cert, keyPair)
|
||||
val cert = Crypto.createCertificate(subject, keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 2)
|
||||
return CertificateAndKeyPair(cert, keyPair)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createSelfSignedCACert(subject: X500Name, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME,
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): CertificateAndKeyPair
|
||||
= createSelfSignedCACert(subject, generateKeyPair(signatureScheme), validityWindow)
|
||||
|
||||
/**
|
||||
* Create a de novo root intermediate X509 v3 CA cert and KeyPair.
|
||||
* @param subject subject of the generated certificate.
|
||||
* @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it
|
||||
* @param ca The Public certificate and KeyPair of the root CA certificate above this used to sign it.
|
||||
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
|
||||
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
|
||||
* @return A data class is returned containing the new intermediate CA Cert and its KeyPair for signing downstream certificates.
|
||||
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates
|
||||
* Note the generated certificate tree is capped at max depth of 1 below this to be in line with commercially available certificates.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createIntermediateCert(subject: X500Name, ca: CertificateAndKey, signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME, validityWindow: Pair<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 issuer = X509CertificateHolder(ca.certificate.encoded).subject
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter)
|
||||
val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, signatureScheme, window, pathLength = 1)
|
||||
return CertificateAndKey(cert, keyPair)
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate)
|
||||
val cert = Crypto.createCertificate(issuer, ca.keyPair, subject, keyPair.public, CA_KEY_USAGE, CA_KEY_PURPOSES, window, pathLength = 1)
|
||||
return CertificateAndKeyPair(cert, keyPair)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an X509v3 certificate suitable for use in TLS roles.
|
||||
* @param subject The contents to put in the subject field of the certificate
|
||||
* @param publicKey The PublicKey to be wrapped in the certificate
|
||||
* @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate
|
||||
* @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes
|
||||
* @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes
|
||||
* @param signatureScheme The signature scheme which will be used to generate keys and certificate. Default to [DEFAULT_TLS_SIGNATURE_SCHEME] if not provided.
|
||||
* @param subject The contents to put in the subject field of the certificate.
|
||||
* @param publicKey The PublicKey to be wrapped in the certificate.
|
||||
* @param ca The Public certificate and KeyPair of the parent CA that will sign this certificate.
|
||||
* @param subjectAlternativeNameDomains A set of alternate DNS names to be supported by the certificate during validation of the TLS handshakes.
|
||||
* @param subjectAlternativeNameIps A set of alternate IP addresses to be supported by the certificate during validation of the TLS handshakes.
|
||||
* @param validityWindow The certificate's validity window. Default to [DEFAULT_VALIDITY_WINDOW] if not provided.
|
||||
* @return The generated X509Certificate suitable for use as a Server/Client certificate in TLS.
|
||||
* This certificate is not marked as a CA cert to be similar in nature to commercial certificates.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createServerCert(subject: X500Name, publicKey: PublicKey,
|
||||
ca: CertificateAndKey,
|
||||
subjectAlternativeNameDomains: List<String>,
|
||||
subjectAlternativeNameIps: List<String>,
|
||||
signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME,
|
||||
validityWindow: Pair<Int, Int> = DEFAULT_VALIDITY_WINDOW): X509Certificate {
|
||||
fun createTlsServerCert(subject: X500Name, publicKey: PublicKey,
|
||||
ca: CertificateAndKeyPair,
|
||||
subjectAlternativeNameDomains: List<String>,
|
||||
subjectAlternativeNameIps: List<String>,
|
||||
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509Certificate {
|
||||
|
||||
val issuer = X509CertificateHolder(ca.certificate.encoded).subject
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate.notBefore, ca.certificate.notAfter)
|
||||
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, ca.certificate)
|
||||
val dnsNames = subjectAlternativeNameDomains.map { GeneralName(GeneralName.dNSName, it) }
|
||||
val ipAddresses = subjectAlternativeNameIps.filter {
|
||||
IPAddress.isValidIPv6WithNetmask(it) || IPAddress.isValidIPv6(it) || IPAddress.isValidIPv4WithNetmask(it) || IPAddress.isValidIPv4(it)
|
||||
}.map { GeneralName(GeneralName.iPAddress, it) }
|
||||
return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, signatureScheme, window, subjectAlternativeName = dnsNames + ipAddresses)
|
||||
return Crypto.createCertificate(issuer, ca.keyPair, subject, publicKey, CLIENT_KEY_USAGE, CLIENT_KEY_PURPOSES, window, subjectAlternativeName = dnsNames + ipAddresses)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection
|
||||
* @param x509Certificate certificate to save
|
||||
* @param filename Target filename
|
||||
* Build a certificate path from a trusted root certificate to a target certificate. This will always return a path
|
||||
* directly from the root to the target, with no intermediate certificates (presuming that path is valid).
|
||||
*
|
||||
* @param rootCertAndKey trusted root certificate that will be the start of the path.
|
||||
* @param targetCertAndKey certificate the path ends at.
|
||||
* @param revocationEnabled whether revocation of certificates in the path should be checked.
|
||||
*/
|
||||
fun createCertificatePath(rootCertAndKey: CertificateAndKeyPair,
|
||||
targetCertAndKey: X509Certificate,
|
||||
revocationEnabled: Boolean): CertPathBuilderResult {
|
||||
val intermediateCertificates = setOf(targetCertAndKey)
|
||||
val certStore = CertStore.getInstance("Collection", CollectionCertStoreParameters(intermediateCertificates))
|
||||
val certPathFactory = CertPathBuilder.getInstance("PKIX")
|
||||
val trustAnchor = TrustAnchor(rootCertAndKey.certificate, null)
|
||||
val certPathParameters = try {
|
||||
PKIXBuilderParameters(setOf(trustAnchor), X509CertSelector().apply {
|
||||
certificate = targetCertAndKey
|
||||
})
|
||||
} catch (ex: InvalidAlgorithmParameterException) {
|
||||
throw RuntimeException(ex)
|
||||
}.apply {
|
||||
addCertStore(certStore)
|
||||
isRevocationEnabled = revocationEnabled
|
||||
}
|
||||
return certPathFactory.build(certPathParameters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to store a .pem/.cer format file copy of a certificate if required for import into a PC/Mac, or for inspection.
|
||||
* @param x509Certificate certificate to save.
|
||||
* @param filename Target filename.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) {
|
||||
@ -172,9 +222,9 @@ object X509Utilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to load back a .pem/.cer format file copy of a certificate
|
||||
* @param filename Source filename
|
||||
* @return The X509Certificate that was encoded in the file
|
||||
* Helper method to load back a .pem/.cer format file copy of a certificate.
|
||||
* @param filename Source filename.
|
||||
* @return The X509Certificate that was encoded in the file.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun loadCertificateFromPEMFile(filename: Path): X509Certificate {
|
||||
@ -186,14 +236,14 @@ object X509Utilities {
|
||||
}
|
||||
|
||||
/**
|
||||
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine
|
||||
* @param keyStoreFilePath KeyStore path to save output to
|
||||
* @param storePassword access password for KeyStore
|
||||
* An all in wrapper to manufacture a server certificate and keys all stored in a KeyStore suitable for running TLS on the local machine.
|
||||
* @param keyStoreFilePath KeyStore path to save output to.
|
||||
* @param storePassword access password for KeyStore.
|
||||
* @param keyPassword PrivateKey access password for the generated keys.
|
||||
* It is recommended that this is the same as the storePassword as most TLS libraries assume they are the same.
|
||||
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore
|
||||
* @param caKeyPassword password to unlock private keys in the CA KeyStore
|
||||
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications
|
||||
* @param caKeyStore KeyStore containing CA keys generated by createCAKeyStoreAndTrustStore.
|
||||
* @param caKeyPassword password to unlock private keys in the CA KeyStore.
|
||||
* @return The KeyStore created containing a private key, certificate chain and root CA public cert for use in TLS applications.
|
||||
*/
|
||||
fun createKeystoreForSSL(keyStoreFilePath: Path,
|
||||
storePassword: String,
|
||||
@ -203,12 +253,12 @@ object X509Utilities {
|
||||
commonName: X500Name,
|
||||
signatureScheme: SignatureScheme = DEFAULT_TLS_SIGNATURE_SCHEME): KeyStore {
|
||||
|
||||
val rootCA = caKeyStore.getCertificateAndKey(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword)
|
||||
val intermediateCA = caKeyStore.getCertificateAndKey(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword)
|
||||
val rootCA = caKeyStore.getCertificateAndKeyPair(CORDA_ROOT_CA_PRIVATE_KEY, caKeyPassword)
|
||||
val intermediateCA = caKeyStore.getCertificateAndKeyPair(CORDA_INTERMEDIATE_CA_PRIVATE_KEY, caKeyPassword)
|
||||
|
||||
val serverKey = generateKeyPair(signatureScheme)
|
||||
val host = InetAddress.getLocalHost()
|
||||
val serverCert = createServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress), signatureScheme)
|
||||
val serverCert = createTlsServerCert(commonName, serverKey.public, intermediateCA, listOf(host.hostName), listOf(host.hostAddress))
|
||||
|
||||
val keyPass = keyPassword.toCharArray()
|
||||
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(keyStoreFilePath, storePassword)
|
||||
@ -228,7 +278,7 @@ object X509Utilities {
|
||||
|
||||
/**
|
||||
* Rebuild the distinguished name, adding a postfix to the common name. If no common name is present, this throws an
|
||||
* exception
|
||||
* exception.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName { attr -> attr.toString() + commonName }
|
||||
@ -238,7 +288,7 @@ fun X500Name.appendToCommonName(commonName: String): X500Name = mutateCommonName
|
||||
* adds one.
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { attr -> commonName }
|
||||
fun X500Name.replaceCommonName(commonName: String): X500Name = mutateCommonName { _ -> commonName }
|
||||
|
||||
/**
|
||||
* Rebuild the distinguished name, replacing the common name with a value generated from the provided function.
|
||||
@ -267,6 +317,7 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N
|
||||
}
|
||||
|
||||
val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString()
|
||||
val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString()
|
||||
val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString()
|
||||
|
||||
class CertificateStream(val input: InputStream) {
|
||||
@ -275,4 +326,4 @@ class CertificateStream(val input: InputStream) {
|
||||
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
|
||||
}
|
||||
|
||||
data class CertificateAndKey(val certificate: X509Certificate, val keyPair: KeyPair)
|
||||
data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair)
|
||||
|
@ -9,11 +9,12 @@ import net.corda.core.serialization.CordaSerializable
|
||||
* the flow to run at the scheduled time.
|
||||
*/
|
||||
interface FlowLogicRefFactory {
|
||||
fun create(type: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
fun create(flowClass: Class<out FlowLogic<*>>, vararg args: Any?): FlowLogicRef
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
||||
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException(
|
||||
"${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
||||
|
||||
/**
|
||||
* A handle interface representing a [FlowLogic] instance which would be possible to safely pass out of the contract sandbox.
|
||||
|
14
core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt
Normal file
14
core/src/main/kotlin/net/corda/core/flows/SchedulableFlow.kt
Normal 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
|
15
core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt
Normal file
15
core/src/main/kotlin/net/corda/core/flows/StartableByRPC.kt
Normal 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
|
@ -16,7 +16,6 @@ abstract class AbstractParty(val owningKey: PublicKey) {
|
||||
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
|
||||
|
||||
override fun hashCode(): Int = owningKey.hashCode()
|
||||
abstract fun toAnonymous(): AnonymousParty
|
||||
abstract fun nameOrNull(): X500Name?
|
||||
|
||||
abstract fun ref(bytes: OpaqueBytes): PartyAndReference
|
||||
|
@ -18,5 +18,4 @@ class AnonymousParty(owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
override fun nameOrNull(): X500Name? = null
|
||||
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
override fun toAnonymous() = this
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.identity
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.crypto.CertificateAndKeyPair
|
||||
import net.corda.core.crypto.toBase58String
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
@ -25,11 +26,10 @@ import java.security.PublicKey
|
||||
*
|
||||
* @see CompositeKey
|
||||
*/
|
||||
// TODO: Remove "open" from [Party] once deprecated crypto.Party class is removed
|
||||
open class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey)
|
||||
override fun toString() = "${owningKey.toBase58String()} ($name)"
|
||||
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
|
||||
constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public)
|
||||
override fun toString() = name.toString()
|
||||
override fun nameOrNull(): X500Name? = name
|
||||
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes)
|
||||
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||
}
|
||||
|
@ -148,14 +148,14 @@ interface CordaRPCOps : RPCOps {
|
||||
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
|
||||
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
|
||||
* result of running the flow.
|
||||
* result of running the flow. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
|
||||
*/
|
||||
@RPCReturnsObservables
|
||||
fun <T : Any> startTrackedFlowDynamic(logicType: Class<out FlowLogic<T>>, vararg args: Any?): FlowProgressHandle<T>
|
||||
|
@ -8,42 +8,38 @@ import java.util.function.Function
|
||||
* Implement this interface on a class advertised in a META-INF/services/net.corda.core.node.CordaPluginRegistry file
|
||||
* to extend a Corda node with additional application services.
|
||||
*/
|
||||
abstract class CordaPluginRegistry(
|
||||
/**
|
||||
* List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should
|
||||
* potentially be able to live in a process separate from the node itself.
|
||||
*/
|
||||
open val webApis: List<Function<CordaRPCOps, out Any>> = emptyList(),
|
||||
abstract class CordaPluginRegistry {
|
||||
/**
|
||||
* List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should
|
||||
* potentially be able to live in a process separate from the node itself.
|
||||
*/
|
||||
open val webApis: List<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 "\*.
|
||||
* 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()
|
||||
*/
|
||||
open val staticServeDirs: Map<String, String> = emptyMap(),
|
||||
/**
|
||||
* Map of static serving endpoints to the matching resource directory. All endpoints will be prefixed with "/web" and postfixed with "\*.
|
||||
* Resource directories can be either on disk directories (especially when debugging) in the form "a/b/c". Serving from a JAR can
|
||||
* be specified with: javaClass.getResource("<folder-in-jar>").toExternalForm()
|
||||
*/
|
||||
open val staticServeDirs: Map<String, String> get() = emptyMap()
|
||||
|
||||
/**
|
||||
* A Map with an entry for each consumed Flow used by the webAPIs.
|
||||
* The key of each map entry should contain the FlowLogic<T> class name.
|
||||
* The associated map values are the union of all concrete class names passed to the Flow constructor.
|
||||
* Standard java.lang.* and kotlin.* types do not need to be included explicitly.
|
||||
* This is used to extend the white listed Flows that can be initiated from the ServiceHub invokeFlowAsync method.
|
||||
*/
|
||||
open val requiredFlows: Map<String, Set<String>> = emptyMap(),
|
||||
@Suppress("unused")
|
||||
@Deprecated("This is no longer needed. Instead annotate any flows that need to be invoked via RPC with " +
|
||||
"@StartableByRPC and any scheduled flows with @SchedulableFlow", level = DeprecationLevel.ERROR)
|
||||
open val requiredFlows: Map<String, Set<String>> get() = emptyMap()
|
||||
|
||||
/**
|
||||
* List of lambdas constructing additional long lived services to be hosted within the node.
|
||||
* They expect a single [PluginServiceHub] parameter as input.
|
||||
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
|
||||
* allow access to the Flow factory and Flow initiation entry points there.
|
||||
*/
|
||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
|
||||
|
||||
/**
|
||||
* List of lambdas constructing additional long lived services to be hosted within the node.
|
||||
* They expect a single [PluginServiceHub] parameter as input.
|
||||
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
|
||||
* allow access to the Flow factory and Flow initiation entry points there.
|
||||
*/
|
||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList()
|
||||
) {
|
||||
/**
|
||||
* Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized.
|
||||
*
|
||||
* For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by
|
||||
* adding the @CordaSerializable annotation or via this method.
|
||||
* For example, if you add a new [net.corda.core.contracts.ContractState] it needs to be whitelisted. You can do that
|
||||
* either by adding the [net.corda.core.serialization.CordaSerializable] annotation or via this method.
|
||||
**
|
||||
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
||||
*/
|
||||
|
@ -1,10 +1,11 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.node.services.*
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import java.security.KeyPair
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
@ -82,23 +83,107 @@ interface ServiceHub : ServicesForResolution {
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
||||
* public legalIdentity Party from the key management service.
|
||||
* Helper property to shorten code for fetching the the [PublicKey] portion of the
|
||||
* Node's primary signing identity.
|
||||
* Typical use is during signing in flows and for unit test signing.
|
||||
*
|
||||
* TODO: legalIdentity can now be composed of multiple keys, should we return a list of keyPairs here? Right now
|
||||
* the logic assumes the legal identity has a composite key with only one node
|
||||
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
|
||||
* the matching [PrivateKey] will be looked up internally and used to sign.
|
||||
* If the key is actually a CompositeKey, the first leaf key hosted on this node
|
||||
* will be used to create the signature.
|
||||
*/
|
||||
val legalIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.legalIdentity.owningKey.keys)
|
||||
val legalIdentityKey: PublicKey get() = this.myInfo.legalIdentity.owningKey
|
||||
|
||||
/**
|
||||
* Helper property to shorten code for fetching the Node's KeyPair associated with the
|
||||
* public notaryIdentity Party from the key management service. It is assumed that this is only
|
||||
* used in contexts where the Node knows it is hosting a Notary Service. Otherwise, it will throw
|
||||
* an IllegalArgumentException.
|
||||
* Helper property to shorten code for fetching the the [PublicKey] portion of the
|
||||
* Node's Notary signing identity. It is required that the Node hosts a notary service,
|
||||
* otherwise an IllegalArgumentException will be thrown.
|
||||
* Typical use is during signing in flows and for unit test signing.
|
||||
*
|
||||
* TODO: same problem as with legalIdentityKey.
|
||||
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
|
||||
* the matching [PrivateKey] will be looked up internally and used to sign.
|
||||
* If the key is actually a [CompositeKey], the first leaf key hosted on this node
|
||||
* will be used to create the signature.
|
||||
*/
|
||||
val notaryIdentityKey: KeyPair get() = this.keyManagementService.toKeyPair(this.myInfo.notaryIdentity.owningKey.keys)
|
||||
}
|
||||
val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey
|
||||
|
||||
/**
|
||||
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
|
||||
* using keys stored inside the node.
|
||||
* @param builder The [TransactionBuilder] to seal with the node's signature.
|
||||
* Any existing signatures on the builder will be preserved.
|
||||
* @param publicKey The [PublicKey] matched to the internal [PrivateKey] to use in signing this transaction.
|
||||
* If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node
|
||||
* to sign with.
|
||||
* @return Returns a SignedTransaction with the new node signature attached.
|
||||
*/
|
||||
fun signInitialTransaction(builder: TransactionBuilder, publicKey: PublicKey): SignedTransaction {
|
||||
val sig = keyManagementService.sign(builder.toWireTransaction().id.bytes, publicKey)
|
||||
builder.addSignatureUnchecked(sig)
|
||||
return builder.toSignedTransaction(false)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to construct an initial partially signed transaction from a TransactionBuilder
|
||||
* using the default identity key contained in the node.
|
||||
* @param builder The TransactionBuilder to seal with the node's signature.
|
||||
* Any existing signatures on the builder will be preserved.
|
||||
* @return Returns a SignedTransaction with the new node signature attached.
|
||||
*/
|
||||
fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey)
|
||||
|
||||
|
||||
/**
|
||||
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
|
||||
* using a set of keys all held in this node.
|
||||
* @param builder The [TransactionBuilder] to seal with the node's signature.
|
||||
* Any existing signatures on the builder will be preserved.
|
||||
* @param signingPubKeys A list of [PublicKeys] used to lookup the matching [PrivateKey] and sign.
|
||||
* @throws IllegalArgumentException is thrown if any keys are unavailable locally.
|
||||
* @return Returns a [SignedTransaction] with the new node signature attached.
|
||||
*/
|
||||
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: List<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)
|
||||
}
|
@ -1,10 +1,13 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.AnonymousParty
|
||||
import net.corda.core.identity.Party
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
|
||||
/**
|
||||
* An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports
|
||||
@ -14,6 +17,29 @@ import java.security.PublicKey
|
||||
interface IdentityService {
|
||||
fun registerIdentity(party: Party)
|
||||
|
||||
/**
|
||||
* Verify and then store the certificates proving that an anonymous party's key is owned by the given full
|
||||
* party.
|
||||
*
|
||||
* @param trustedRoot trusted root certificate, typically the R3 master signing certificate.
|
||||
* @param anonymousParty an anonymised party belonging to the legal entity.
|
||||
* @param path certificate path from the trusted root to the anonymised party.
|
||||
* @throws IllegalArgumentException if the chain does not link the two parties, or if there is already an existing
|
||||
* certificate chain for the anonymous party. Anonymous parties must always resolve to a single owning party.
|
||||
*/
|
||||
// TODO: Move this into internal identity service once available
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath)
|
||||
|
||||
/**
|
||||
* Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with
|
||||
* the anonymous party and resolving it back to the given full party.
|
||||
*
|
||||
* @throws IllegalStateException if the anonymous party is not owned by the full party.
|
||||
*/
|
||||
@Throws(IllegalStateException::class)
|
||||
fun assertOwnership(party: Party, anonymousParty: AnonymousParty)
|
||||
|
||||
/**
|
||||
* Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be
|
||||
* used in preference where possible.
|
||||
@ -29,6 +55,13 @@ interface IdentityService {
|
||||
fun partyFromName(name: String): Party?
|
||||
fun partyFromX500Name(principal: X500Name): Party?
|
||||
|
||||
fun partyFromAnonymous(party: AnonymousParty): Party?
|
||||
fun partyFromAnonymous(party: AbstractParty): Party?
|
||||
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
|
||||
|
||||
/**
|
||||
* Get the certificate chain showing an anonymous party is owned by the given party.
|
||||
*/
|
||||
fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath?
|
||||
|
||||
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
|
||||
}
|
||||
|
@ -3,7 +3,8 @@ package net.corda.core.node.services
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
@ -18,8 +19,6 @@ import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import rx.Observable
|
||||
import java.io.InputStream
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -286,7 +285,7 @@ interface VaultService {
|
||||
@Suspendable
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
to: PublicKey,
|
||||
to: AbstractParty,
|
||||
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<PublicKey>>
|
||||
|
||||
// DOCSTART VaultStatesQuery
|
||||
@ -371,32 +370,32 @@ class StatesNotAvailableException(override val message: String?, override val ca
|
||||
/**
|
||||
* The KMS is responsible for storing and using private keys to sign things. An implementation of this may, for example,
|
||||
* call out to a hardware security module that enforces various auditing and frequency-of-use requirements.
|
||||
*
|
||||
* The current interface is obviously not usable for those use cases: this is just where we'd put a real signing
|
||||
* interface if/when one is developed.
|
||||
*/
|
||||
|
||||
interface KeyManagementService {
|
||||
/** Returns a snapshot of the current pubkey->privkey mapping. */
|
||||
val keys: Map<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)
|
||||
fun toKeyPair(publicKey: PublicKey): KeyPair {
|
||||
when (publicKey) {
|
||||
is CompositeKey -> throw IllegalArgumentException("Got CompositeKey when single PublicKey expected.")
|
||||
else -> return KeyPair(publicKey, toPrivate(publicKey))
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the first [KeyPair] matching any of the [publicKeys] */
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun toKeyPair(publicKeys: Iterable<PublicKey>) = publicKeys.first { keys.contains(it) }.let { toKeyPair(it) }
|
||||
|
||||
/** Generates a new random key and adds it to the exposed map. */
|
||||
fun freshKey(): KeyPair
|
||||
/** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data.
|
||||
* @param bytes The data to sign over using the chosen key.
|
||||
* @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
|
||||
* or previously generated via the [freshKey] method.
|
||||
* If the [PublicKey] is actually a [CompositeKey] the first leaf signing key hosted by the node is used.
|
||||
* @throws IllegalArgumentException if the input key is not a member of [keys].
|
||||
* TODO A full [KeyManagementService] implementation needs to record activity to the [AuditService] and to limit signing to
|
||||
* appropriately authorised contexts and initiating users.
|
||||
*/
|
||||
@Suspendable
|
||||
fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey
|
||||
}
|
||||
|
||||
// TODO: Move to a more appropriate location
|
||||
|
@ -37,9 +37,9 @@ interface QueryableState : ContractState {
|
||||
* @param version The version number of this instance within the family.
|
||||
* @param mappedTypes The JPA entity classes that the ORM layer needs to be configure with for this schema.
|
||||
*/
|
||||
abstract class MappedSchema(schemaFamily: Class<*>,
|
||||
val version: Int,
|
||||
val mappedTypes: Iterable<Class<*>>) {
|
||||
open class MappedSchema(schemaFamily: Class<*>,
|
||||
val version: Int,
|
||||
val mappedTypes: Iterable<Class<*>>) {
|
||||
val name: String = schemaFamily.name
|
||||
override fun toString(): String = "${this.javaClass.simpleName}(name=$name, version=$version)"
|
||||
}
|
||||
|
@ -26,9 +26,12 @@ import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||
import org.slf4j.Logger
|
||||
import sun.security.provider.certpath.X509CertPath
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.util.*
|
||||
|
||||
object DefaultKryoCustomizer {
|
||||
@ -97,6 +100,12 @@ object DefaultKryoCustomizer {
|
||||
// Note that return type should be specifically set to InputStream, otherwise it may not work, i.e. val aStream : InputStream = HashCheckingStream(...).
|
||||
addDefaultSerializer(InputStream::class.java, InputStreamSerializer)
|
||||
|
||||
register(CertPath::class.java, CertPathSerializer)
|
||||
register(X509CertPath::class.java, CertPathSerializer)
|
||||
// TODO: We shouldn't need to serialize raw certificates, and if we do then we need a cleaner solution
|
||||
// than this mess.
|
||||
val x509CertObjectClazz = Class.forName("org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject")
|
||||
register(x509CertObjectClazz, X509CertificateSerializer)
|
||||
register(X500Name::class.java, X500NameSerializer)
|
||||
|
||||
register(BCECPrivateKey::class.java, PrivateKeySerializer)
|
||||
|
@ -31,6 +31,9 @@ import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.PrivateKey
|
||||
import java.security.PublicKey
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
@ -617,6 +620,36 @@ object X500NameSerializer : Serializer<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 {
|
||||
override fun <T : Any?> run(callback: KryoCallback<T>): T {
|
||||
val kryo = borrow()
|
||||
|
@ -1,13 +1,16 @@
|
||||
package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.MerkleTreeException
|
||||
import net.corda.core.crypto.PartialMerkleTree
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.p2PKryo
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.security.PublicKey
|
||||
import net.corda.core.serialization.withoutReferences
|
||||
import java.security.PublicKey
|
||||
|
||||
fun <T : Any> serializedHash(x: T): SecureHash {
|
||||
return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } }
|
||||
@ -91,7 +94,7 @@ class FilteredLeaves(
|
||||
*/
|
||||
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
|
||||
val checkList = availableComponents.map { checkingFun(it) }
|
||||
return (!checkList.isEmpty()) && checkList.all { true }
|
||||
return (!checkList.isEmpty()) && checkList.all { it }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,14 +3,12 @@ package net.corda.core.transactions
|
||||
import net.corda.core.contracts.AttachmentResolutionException
|
||||
import net.corda.core.contracts.NamedByHash
|
||||
import net.corda.core.contracts.TransactionResolutionException
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
@ -146,14 +144,5 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class)
|
||||
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services)
|
||||
|
||||
/**
|
||||
* Utility to simplify the act of signing the transaction.
|
||||
*
|
||||
* @param keyPair the signer's public/private key pair.
|
||||
*
|
||||
* @return a digital signature of the transaction.
|
||||
*/
|
||||
fun signWithECDSA(keyPair: KeyPair) = keyPair.sign(this.id.bytes)
|
||||
|
||||
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||
}
|
||||
|
@ -59,8 +59,10 @@ class LazyPool<A>(
|
||||
* the returned iterable will be inaccurate.
|
||||
*/
|
||||
fun close(): Iterable<A> {
|
||||
lifeCycle.transition(State.STARTED, State.FINISHED)
|
||||
return poolQueue
|
||||
lifeCycle.justTransition(State.FINISHED)
|
||||
val elements = poolQueue.toList()
|
||||
poolQueue.clear()
|
||||
return elements
|
||||
}
|
||||
|
||||
inline fun <R> run(withInstance: (A) -> R): R {
|
||||
|
@ -24,12 +24,14 @@ fun validateLegalName(normalizedLegalName: String) {
|
||||
rules.forEach { it.validate(normalizedLegalName) }
|
||||
}
|
||||
|
||||
val WHITESPACE = "\\s++".toRegex()
|
||||
|
||||
/**
|
||||
* The normalize function will trim the input string, replace any multiple spaces with a single space,
|
||||
* and normalize the string according to NFKC normalization form.
|
||||
*/
|
||||
fun normaliseLegalName(legalName: String): String {
|
||||
val trimmedLegalName = legalName.trim().replace(Regex("\\s+"), " ")
|
||||
val trimmedLegalName = legalName.trim().replace(WHITESPACE, " ")
|
||||
return Normalizer.normalize(trimmedLegalName, Normalizer.Form.NFKC)
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ class LifeCycle<S : Enum<S>>(initial: S) {
|
||||
private val lock = ReentrantReadWriteLock()
|
||||
private var state = initial
|
||||
|
||||
/** Assert that the lifecycle in the [requiredState] */
|
||||
/** Assert that the lifecycle in the [requiredState]. */
|
||||
fun requireState(requiredState: S) {
|
||||
requireState({ "Required state to be $requiredState, was $it" }) { it == requiredState }
|
||||
}
|
||||
@ -28,11 +28,18 @@ class LifeCycle<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) {
|
||||
lock.writeLock().withLock {
|
||||
require(state == from) { "Required state to be $from to transition to $to, was $state" }
|
||||
state = to
|
||||
}
|
||||
}
|
||||
|
||||
/** Transition the state to [to] without performing a current state check. */
|
||||
fun justTransition(to: S) {
|
||||
lock.writeLock().withLock {
|
||||
state = to
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,11 @@ import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.isFulfilledBy
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
@ -59,13 +61,13 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
|
||||
val myKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
val myKey = serviceHub.myInfo.legalIdentity
|
||||
val me = listOf(myKey)
|
||||
|
||||
val signatures = if (participants == me) {
|
||||
getNotarySignatures(stx)
|
||||
} else {
|
||||
collectSignatures(participants - me, stx)
|
||||
collectSignatures((participants - me).map { it.owningKey }, stx)
|
||||
}
|
||||
|
||||
val finalTx = stx + signatures
|
||||
@ -73,7 +75,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
return finalTx.tx.outRef(0)
|
||||
}
|
||||
|
||||
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<PublicKey>>
|
||||
abstract protected fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>>
|
||||
|
||||
@Suspendable
|
||||
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 {
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
return myKey.sign(stx.id)
|
||||
return serviceHub.createSignature(stx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
259
core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
Normal file
259
core/src/main/kotlin/net/corda/flows/CollectSignaturesFlow.kt
Normal 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}"
|
||||
}
|
||||
}
|
||||
}
|
@ -2,6 +2,8 @@ package net.corda.flows
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.flows.StartableByRPC
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import java.security.PublicKey
|
||||
@ -15,6 +17,7 @@ import java.security.PublicKey
|
||||
* use the new updated state for future transactions.
|
||||
*/
|
||||
@InitiatingFlow
|
||||
@StartableByRPC
|
||||
class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState>(
|
||||
originalState: StateAndRef<OldState>,
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
@ -30,12 +33,12 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
|
||||
@JvmStatic
|
||||
fun verify(input: ContractState, output: ContractState, commandData: Command) {
|
||||
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()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participants)
|
||||
"The signing keys include all participant keys" using keysThatSigned.containsAll(participantKeys)
|
||||
"Inputs state reference the legacy contract" using (input.contract.javaClass == upgradedContract.legacyContract)
|
||||
"Outputs state reference the upgraded contract" using (output.contract.javaClass == command.upgradedContractClass)
|
||||
"Output state must be an upgraded version of the input state" using (output == upgradedContract.upgrade(input))
|
||||
@ -51,14 +54,13 @@ class ContractUpgradeFlow<OldState : ContractState, out NewState : ContractState
|
||||
.withItems(
|
||||
stateRef,
|
||||
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>> {
|
||||
val stx = assembleBareTx(originalState, modification)
|
||||
.signWith(serviceHub.legalIdentityKey)
|
||||
.toSignedTransaction(false)
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<AbstractParty>> {
|
||||
val baseTx = assembleBareTx(originalState, modification)
|
||||
val stx = serviceHub.signInitialTransaction(baseTx)
|
||||
return stx to originalState.state.data.participants
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
|
||||
/**
|
||||
* Verifies the given transactions, then sends them to the named notaries. If the notary agrees that the transactions
|
||||
* Verifies the given transactions, then sends them to the named notary. If the notary agrees that the transactions
|
||||
* are acceptable then they are from that point onwards committed to the ledger, and will be written through to the
|
||||
* vault. Additionally they will be distributed to the parties reflected in the participants list of the states.
|
||||
*
|
||||
@ -37,6 +37,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
override val progressTracker: ProgressTracker) : FlowLogic<List<SignedTransaction>>() {
|
||||
constructor(transaction: SignedTransaction, extraParticipants: Set<Party>) : this(listOf(transaction), extraParticipants, tracker())
|
||||
constructor(transaction: SignedTransaction) : this(listOf(transaction), emptySet(), tracker())
|
||||
constructor(transaction: SignedTransaction, progressTracker: ProgressTracker) : this(listOf(transaction), emptySet(), progressTracker)
|
||||
|
||||
companion object {
|
||||
object NOTARISING : ProgressTracker.Step("Requesting signature by notary service") {
|
||||
@ -105,7 +106,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
// Calculate who is meant to see the results based on the participants involved.
|
||||
val keys = ltx.outputs.flatMap { it.data.participants } + ltx.inputs.flatMap { it.state.data.participants }
|
||||
// TODO: Is it safe to drop participants we don't know how to contact? Does not knowing how to contact them count as a reason to fail?
|
||||
val parties = keys.mapNotNull { serviceHub.identityService.partyFromKey(it) }.toSet()
|
||||
val parties = keys.mapNotNull { serviceHub.identityService.partyFromAnonymous(it) }.toSet()
|
||||
Pair(stx, parties)
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package net.corda.flows
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
@ -24,11 +25,11 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
progressTracker: ProgressTracker = tracker())
|
||||
: 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 tx = TransactionType.NotaryChange.Builder(originalState.state.notary)
|
||||
|
||||
val participants: Iterable<PublicKey>
|
||||
val participants: Iterable<AbstractParty>
|
||||
|
||||
if (state.encumbrance == null) {
|
||||
val modifiedState = TransactionState(state.data, modification)
|
||||
@ -39,10 +40,7 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
participants = resolveEncumbrances(tx)
|
||||
}
|
||||
|
||||
val myKey = serviceHub.legalIdentityKey
|
||||
tx.signWith(myKey)
|
||||
|
||||
val stx = tx.toSignedTransaction(false)
|
||||
val stx = serviceHub.signInitialTransaction(tx)
|
||||
|
||||
return Pair(stx, participants)
|
||||
}
|
||||
@ -53,14 +51,14 @@ class NotaryChangeFlow<out T : ContractState>(
|
||||
*
|
||||
* @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 txId = stateRef.txhash
|
||||
val issuingTx = serviceHub.storageService.validatedTransactions.getTransaction(txId)
|
||||
?: throw StateReplacementException("Transaction $txId not found")
|
||||
val outputs = issuingTx.tx.outputs
|
||||
|
||||
val participants = mutableSetOf<PublicKey>()
|
||||
val participants = mutableSetOf<AbstractParty>()
|
||||
|
||||
var nextStateIndex = stateRef.index
|
||||
var newOutputPosition = tx.outputStates().size
|
||||
|
@ -3,7 +3,10 @@ package net.corda.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.Timestamp
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.keys
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
@ -144,8 +147,7 @@ object NotaryFlow {
|
||||
}
|
||||
|
||||
private fun sign(bits: ByteArray): DigitalSignature.WithKey {
|
||||
val mySigningKey = serviceHub.notaryIdentityKey
|
||||
return mySigningKey.sign(bits)
|
||||
return serviceHub.keyManagementService.sign(bits, serviceHub.notaryIdentityKey)
|
||||
}
|
||||
|
||||
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
|
||||
|
@ -2,21 +2,21 @@ package net.corda.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.DealState
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.contracts.requireThat
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.expandedCompositeKeys
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.trace
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.KeyPair
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -26,7 +26,7 @@ import java.security.PublicKey
|
||||
*
|
||||
* TODO: Also, the term Deal is used here where we might prefer Agreement.
|
||||
*
|
||||
* TODO: Consider whether we can merge this with [TwoPartyTradeFlow]
|
||||
* TODO: Make this flow more generic.
|
||||
*
|
||||
*/
|
||||
object TwoPartyDealFlow {
|
||||
@ -34,151 +34,57 @@ object TwoPartyDealFlow {
|
||||
@CordaSerializable
|
||||
data class Handshake<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.
|
||||
*
|
||||
* 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>() {
|
||||
|
||||
companion object {
|
||||
object AWAITING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying proposed transaction")
|
||||
object SIGNING : ProgressTracker.Step("Signing transaction")
|
||||
object NOTARY : ProgressTracker.Step("Getting notary signature")
|
||||
object SENDING_SIGS : ProgressTracker.Step("Sending transaction signatures to other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator")
|
||||
|
||||
fun tracker() = ProgressTracker(AWAITING_PROPOSAL, VERIFYING, SIGNING, NOTARY, SENDING_SIGS, RECORDING, COPYING_TO_REGULATOR)
|
||||
object SENDING_PROPOSAL : ProgressTracker.Step("Handshaking and awaiting transaction proposal.")
|
||||
fun tracker() = ProgressTracker(SENDING_PROPOSAL)
|
||||
}
|
||||
|
||||
abstract val payload: Any
|
||||
abstract val notaryNode: NodeInfo
|
||||
abstract val otherParty: Party
|
||||
abstract val myKeyPair: KeyPair
|
||||
|
||||
@Suspendable
|
||||
fun getPartialTransaction(): UntrustworthyData<SignedTransaction> {
|
||||
progressTracker.currentStep = AWAITING_PROPOSAL
|
||||
abstract val myKey: PublicKey
|
||||
|
||||
@Suspendable override fun call(): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_PROPOSAL
|
||||
// Make the first message we'll send to kick off the flow.
|
||||
val hello = Handshake(payload, myKeyPair.public)
|
||||
val maybeSTX = sendAndReceive<SignedTransaction>(otherParty, hello)
|
||||
val hello = Handshake(payload, serviceHub.myInfo.legalIdentity.owningKey)
|
||||
// Wait for the FinalityFlow to finish on the other side and return the tx when it's available.
|
||||
send(otherParty, hello)
|
||||
|
||||
return maybeSTX
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
fun verifyPartialTransaction(untrustedPartialTX: UntrustworthyData<SignedTransaction>): SignedTransaction {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
|
||||
untrustedPartialTX.unwrap { stx ->
|
||||
progressTracker.nextStep()
|
||||
|
||||
// Check that the tx proposed by the buyer is valid.
|
||||
val wtx: WireTransaction = stx.verifySignatures(myKeyPair.public, notaryNode.notaryIdentity.owningKey)
|
||||
logger.trace { "Received partially signed transaction: ${stx.id}" }
|
||||
|
||||
checkDependencies(stx)
|
||||
|
||||
// This verifies that the transaction is contract-valid, even though it is missing signatures.
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
|
||||
// There are all sorts of funny games a malicious secondary might play here, we should fix them:
|
||||
//
|
||||
// - This tx may attempt to send some assets we aren't intending to sell to the secondary, if
|
||||
// we're reusing keys! So don't reuse keys!
|
||||
// - This tx may include output states that impose odd conditions on the movement of the cash,
|
||||
// once we implement state pairing.
|
||||
//
|
||||
// but the goal of this code is not to be fully secure (yet), but rather, just to find good ways to
|
||||
// express flow state machines on top of the messaging layer.
|
||||
|
||||
return stx
|
||||
}
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun checkDependencies(stx: SignedTransaction) {
|
||||
// Download and check all the transactions that this transaction depends on, but do not check this
|
||||
// transaction itself.
|
||||
val dependencyTxIDs = stx.tx.inputs.map { it.txhash }.toSet()
|
||||
subFlow(ResolveTransactionsFlow(dependencyTxIDs, otherParty))
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val stx: SignedTransaction = verifyPartialTransaction(getPartialTransaction())
|
||||
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = computeOurSignature(stx)
|
||||
val allPartySignedTx = stx + ourSignature
|
||||
val notarySignatures = getNotarySignatures(allPartySignedTx)
|
||||
|
||||
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures)
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
|
||||
serviceHub.recordTransactions(fullySigned)
|
||||
|
||||
logger.trace { "Deal stored" }
|
||||
|
||||
progressTracker.currentStep = COPYING_TO_REGULATOR
|
||||
val regulators = serviceHub.networkMapCache.regulatorNodes
|
||||
if (regulators.isNotEmpty()) {
|
||||
// If there are regulators in the network, then we could copy them in on the transaction via a sub-flow
|
||||
// which would simply send them the transaction.
|
||||
val signTransactionFlow = object : SignTransactionFlow(otherParty) {
|
||||
override fun checkTransaction(stx: SignedTransaction) = checkProposal(stx)
|
||||
}
|
||||
|
||||
return fullySigned
|
||||
subFlow(signTransactionFlow)
|
||||
|
||||
val txHash = receive<SecureHash>(otherParty).unwrap { it }
|
||||
|
||||
return waitForLedgerCommit(txHash)
|
||||
}
|
||||
|
||||
@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
|
||||
}
|
||||
@Suspendable abstract fun checkProposal(stx: SignedTransaction)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Abstracted bilateral deal flow participant that is recipient of initial communication.
|
||||
*
|
||||
* There's a good chance we can push at least some of this logic down into core flow logic
|
||||
* and helper methods etc.
|
||||
*/
|
||||
abstract class Secondary<U>(override val progressTracker: ProgressTracker = Secondary.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
|
||||
companion object {
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying deal info")
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal")
|
||||
object SWAPPING_SIGNATURES : ProgressTracker.Step("Swapping signatures with the other party")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction")
|
||||
object RECEIVING : ProgressTracker.Step("Waiting for deal info.")
|
||||
object VERIFYING : ProgressTracker.Step("Verifying deal info.")
|
||||
object SIGNING : ProgressTracker.Step("Generating and signing transaction proposal.")
|
||||
object COLLECTING_SIGNATURES : ProgressTracker.Step("Collecting signatures from other parties.")
|
||||
object RECORDING : ProgressTracker.Step("Recording completed transaction.")
|
||||
object COPYING_TO_REGULATOR : ProgressTracker.Step("Copying regulator.")
|
||||
object COPYING_TO_COUNTERPARTY : ProgressTracker.Step("Copying counterparty.")
|
||||
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, SWAPPING_SIGNATURES, RECORDING)
|
||||
fun tracker() = ProgressTracker(RECEIVING, VERIFYING, SIGNING, COLLECTING_SIGNATURES, RECORDING, COPYING_TO_REGULATOR, COPYING_TO_COUNTERPARTY)
|
||||
}
|
||||
|
||||
abstract val otherParty: Party
|
||||
@ -188,23 +94,35 @@ object TwoPartyDealFlow {
|
||||
val handshake = receiveAndValidateHandshake()
|
||||
|
||||
progressTracker.currentStep = SIGNING
|
||||
val (ptx, additionalSigningPubKeys) = assembleSharedTX(handshake)
|
||||
val stx = signWithOurKeys(additionalSigningPubKeys, ptx)
|
||||
val (utx, additionalSigningPubKeys) = assembleSharedTX(handshake)
|
||||
val ptx = signWithOurKeys(additionalSigningPubKeys, utx)
|
||||
|
||||
val signatures = swapSignaturesWithPrimary(stx)
|
||||
logger.trace { "Signed proposed transaction." }
|
||||
|
||||
progressTracker.currentStep = COLLECTING_SIGNATURES
|
||||
val stx = subFlow(CollectSignaturesFlow(ptx))
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySigs
|
||||
fullySigned.verifySignatures()
|
||||
|
||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
serviceHub.recordTransactions(fullySigned)
|
||||
val ftx = subFlow(FinalityFlow(stx, setOf(otherParty, serviceHub.myInfo.legalIdentity))).single()
|
||||
|
||||
logger.trace { "Deal transaction stored" }
|
||||
return fullySigned
|
||||
logger.trace { "Recorded transaction." }
|
||||
|
||||
progressTracker.currentStep = COPYING_TO_REGULATOR
|
||||
val regulators = serviceHub.networkMapCache.regulatorNodes
|
||||
if (regulators.isNotEmpty()) {
|
||||
// Copy the transaction to every regulator in the network. This is obviously completely bogus, it's
|
||||
// just for demo purposes.
|
||||
regulators.forEach { send(it.serviceIdentities(ServiceType.regulator).first(), ftx) }
|
||||
}
|
||||
|
||||
progressTracker.currentStep = COPYING_TO_COUNTERPARTY
|
||||
// Send the final transaction hash back to the other party.
|
||||
// We need this so we don't break the IRS demo and the SIMM Demo.
|
||||
send(otherParty, ftx.id)
|
||||
|
||||
return ftx
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
@ -217,24 +135,9 @@ object TwoPartyDealFlow {
|
||||
return handshake.unwrap { validateHandshake(it) }
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun swapSignaturesWithPrimary(stx: SignedTransaction): SignaturesFromPrimary {
|
||||
progressTracker.currentStep = SWAPPING_SIGNATURES
|
||||
logger.trace { "Sending partially signed transaction to other party" }
|
||||
|
||||
// TODO: Protect against the seller terminating here and leaving us in the lurch without the final tx.
|
||||
|
||||
return sendAndReceive<SignaturesFromPrimary>(otherParty, stx).unwrap { it }
|
||||
}
|
||||
|
||||
private fun signWithOurKeys(signingPubKeys: List<PublicKey>, ptx: TransactionBuilder): SignedTransaction {
|
||||
// Now sign the transaction with whatever keys we need to move the cash.
|
||||
for (publicKey in signingPubKeys.expandedCompositeKeys) {
|
||||
val privateKey = serviceHub.keyManagementService.toPrivate(publicKey)
|
||||
ptx.signWith(KeyPair(publicKey, privateKey))
|
||||
}
|
||||
|
||||
return ptx.toSignedTransaction(checkSufficientSignatures = false)
|
||||
return serviceHub.signInitialTransaction(ptx, signingPubKeys)
|
||||
}
|
||||
|
||||
@Suspendable protected abstract fun validateHandshake(handshake: Handshake<U>): Handshake<U>
|
||||
@ -244,17 +147,20 @@ object TwoPartyDealFlow {
|
||||
@CordaSerializable
|
||||
data class AutoOffer(val notary: Party, val dealBeingOffered: DealState)
|
||||
|
||||
|
||||
/**
|
||||
* One side of the flow for inserting a pre-agreed deal.
|
||||
*/
|
||||
open class Instigator(override val otherParty: Party,
|
||||
override val payload: AutoOffer,
|
||||
override val myKeyPair: KeyPair,
|
||||
override val myKey: PublicKey,
|
||||
override val progressTracker: ProgressTracker = Primary.tracker()) : Primary() {
|
||||
|
||||
override val notaryNode: NodeInfo get() =
|
||||
serviceHub.networkMapCache.notaryNodes.filter { it.notaryIdentity == payload.notary }.single()
|
||||
|
||||
@Suspendable override fun checkProposal(stx: SignedTransaction) = requireThat {
|
||||
// Add some constraints here.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,5 +187,4 @@ object TwoPartyDealFlow {
|
||||
return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ object TxKeyFlowUtilities {
|
||||
*/
|
||||
@Suspendable
|
||||
fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey {
|
||||
val key = flow.serviceHub.keyManagementService.freshKey().public
|
||||
val key = flow.serviceHub.keyManagementService.freshKey()
|
||||
// TODO: Generate and sign certificate for the key, once we have signing support for composite keys
|
||||
// (in this case the legal identity key)
|
||||
flow.send(otherSide, ProvidedTransactionKey(key, null))
|
||||
|
Binary file not shown.
@ -1,6 +1,7 @@
|
||||
package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import org.junit.Test
|
||||
@ -14,7 +15,7 @@ class DummyContractV2Tests {
|
||||
@Test
|
||||
fun `upgrade from v1`() {
|
||||
val contractUpgrade = DummyContractV2()
|
||||
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||
val v1State = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
|
||||
val v1Ref = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val v1StateAndRef = StateAndRef(v1State, v1Ref)
|
||||
val (tx, _) = DummyContractV2().generateUpgradeFromV1(v1StateAndRef)
|
||||
|
@ -2,13 +2,12 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_2
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.ledger
|
||||
import net.corda.testing.transaction
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.time.Instant
|
||||
import java.time.temporal.ChronoUnit
|
||||
|
||||
@ -19,9 +18,9 @@ class TransactionEncumbranceTests {
|
||||
|
||||
val state = Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` defaultIssuer,
|
||||
owner = DUMMY_PUBKEY_1
|
||||
owner = MEGA_CORP
|
||||
)
|
||||
val stateWithNewOwner = state.copy(owner = DUMMY_PUBKEY_2)
|
||||
val stateWithNewOwner = state.copy(owner = MINI_CORP)
|
||||
|
||||
val FOUR_PM: Instant = Instant.parse("2015-04-17T16:00:00.00Z")
|
||||
val FIVE_PM: Instant = FOUR_PM.plus(1, ChronoUnit.HOURS)
|
||||
@ -40,7 +39,7 @@ class TransactionEncumbranceTests {
|
||||
data class State(
|
||||
val validFrom: Instant
|
||||
) : ContractState {
|
||||
override val participants: List<PublicKey> = emptyList()
|
||||
override val participants: List<AbstractParty> = emptyList()
|
||||
override val contract: Contract = TEST_TIMELOCK_ID
|
||||
}
|
||||
}
|
||||
@ -52,7 +51,7 @@ class TransactionEncumbranceTests {
|
||||
input { state }
|
||||
output(encumbrance = 1) { stateWithNewOwner }
|
||||
output("5pm time-lock") { timeLock }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
verifies()
|
||||
}
|
||||
}
|
||||
@ -70,7 +69,7 @@ class TransactionEncumbranceTests {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
verifies()
|
||||
}
|
||||
@ -89,7 +88,7 @@ class TransactionEncumbranceTests {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
input("5pm time-lock")
|
||||
output { state }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
timestamp(FOUR_PM)
|
||||
this `fails with` "the time specified in the time-lock has passed"
|
||||
}
|
||||
@ -106,7 +105,7 @@ class TransactionEncumbranceTests {
|
||||
transaction {
|
||||
input("state encumbered by 5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
@ -118,7 +117,7 @@ class TransactionEncumbranceTests {
|
||||
transaction {
|
||||
input { state }
|
||||
output(encumbrance = 0) { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 0 in OUTPUT"
|
||||
}
|
||||
}
|
||||
@ -129,7 +128,7 @@ class TransactionEncumbranceTests {
|
||||
input { state }
|
||||
output(encumbrance = 2) { stateWithNewOwner }
|
||||
output { timeLock }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
this `fails with` "Missing required encumbrance 2 in OUTPUT"
|
||||
}
|
||||
}
|
||||
@ -146,7 +145,7 @@ class TransactionEncumbranceTests {
|
||||
input("state encumbered by some other state")
|
||||
input("5pm time-lock")
|
||||
output { stateWithNewOwner }
|
||||
command(DUMMY_PUBKEY_1) { Cash.Commands.Move() }
|
||||
command(MEGA_CORP.owningKey) { Cash.Commands.Move() }
|
||||
timestamp(FIVE_PM)
|
||||
this `fails with` "Missing required encumbrance 1 in INPUT"
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.testing.ALICE_PUBKEY
|
||||
import org.junit.Test
|
||||
import java.security.KeyPair
|
||||
import kotlin.test.assertEquals
|
||||
@ -95,7 +94,7 @@ class TransactionTests {
|
||||
|
||||
@Test
|
||||
fun `transactions with no inputs can have any notary`() {
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
|
||||
val inputs = emptyList<StateAndRef<*>>()
|
||||
val outputs = listOf(baseOutState, baseOutState.copy(notary = ALICE), baseOutState.copy(notary = BOB))
|
||||
val commands = emptyList<AuthenticatedObject<CommandData>>()
|
||||
@ -120,7 +119,7 @@ class TransactionTests {
|
||||
|
||||
@Test
|
||||
fun `transaction verification fails for duplicate inputs`() {
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), DUMMY_NOTARY)
|
||||
val baseOutState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), DUMMY_NOTARY)
|
||||
val stateRef = StateRef(SecureHash.randomSHA256(), 0)
|
||||
val stateAndRef = StateAndRef(baseOutState, stateRef)
|
||||
val inputs = listOf(stateAndRef, stateAndRef)
|
||||
@ -148,7 +147,7 @@ class TransactionTests {
|
||||
@Test
|
||||
fun `general transactions cannot change notary`() {
|
||||
val notary: Party = DUMMY_NOTARY
|
||||
val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE_PUBKEY), notary)
|
||||
val inState = TransactionState(DummyContract.SingleOwnerState(0, ALICE), notary)
|
||||
val outState = inState.copy(notary = ALICE)
|
||||
val inputs = listOf(StateAndRef(inState, StateRef(SecureHash.randomSHA256(), 0)))
|
||||
val outputs = listOf(outState)
|
||||
|
@ -12,9 +12,7 @@ import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_PUBKEY_1
|
||||
import net.corda.core.utilities.TEST_TX_TIME
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MEGA_CORP_PUBKEY
|
||||
import net.corda.testing.ledger
|
||||
import net.corda.testing.*
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.*
|
||||
@ -30,20 +28,20 @@ class PartialMerkleTreeTest {
|
||||
output("MEGA_CORP cash") {
|
||||
Cash.State(
|
||||
amount = 1000.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = MEGA_CORP_PUBKEY
|
||||
owner = MEGA_CORP
|
||||
)
|
||||
}
|
||||
output("dummy cash 1") {
|
||||
Cash.State(
|
||||
amount = 900.DOLLARS `issued by` MEGA_CORP.ref(1, 1),
|
||||
owner = DUMMY_PUBKEY_1
|
||||
owner = MINI_CORP
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
transaction {
|
||||
input("MEGA_CORP cash")
|
||||
output("MEGA_CORP cash".output<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() }
|
||||
timestamp(TEST_TX_TIME)
|
||||
this.verifies()
|
||||
@ -61,7 +59,7 @@ class PartialMerkleTreeTest {
|
||||
|
||||
@Test
|
||||
fun `building Merkle tree - no hashes`() {
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.Companion.getMerkleTree(emptyList()) }
|
||||
assertFailsWith<MerkleTreeException> { MerkleTree.getMerkleTree(emptyList()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -98,7 +96,7 @@ class PartialMerkleTreeTest {
|
||||
fun filtering(elem: Any): Boolean {
|
||||
return when (elem) {
|
||||
is StateRef -> true
|
||||
is TransactionState<*> -> elem.data.participants[0].keys == DUMMY_PUBKEY_1.keys
|
||||
is TransactionState<*> -> elem.data.participants[0].owningKey.keys == MINI_CORP_PUBKEY.keys
|
||||
is Command -> MEGA_CORP_PUBKEY in elem.signers
|
||||
is Timestamp -> true
|
||||
is PublicKey -> elem == MEGA_CORP_PUBKEY
|
||||
|
@ -2,7 +2,6 @@ package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.div
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.corda.testing.getTestX509Name
|
||||
import org.bouncycastle.asn1.x500.X500Name
|
||||
import org.bouncycastle.asn1.x509.GeneralName
|
||||
@ -57,7 +56,7 @@ class X509UtilitiesTest {
|
||||
val caCertAndKey = X509Utilities.createSelfSignedCACert(getTestX509Name("Test CA Cert"))
|
||||
val subjectDN = getTestX509Name("Server Cert")
|
||||
val keyPair = Crypto.generateKeyPair(X509Utilities.DEFAULT_TLS_SIGNATURE_SCHEME)
|
||||
val serverCert = X509Utilities.createServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54"))
|
||||
val serverCert = X509Utilities.createTlsServerCert(subjectDN, keyPair.public, caCertAndKey, listOf("alias name"), listOf("10.0.0.54"))
|
||||
assertTrue { serverCert.subjectDN.name.contains("CN=Server Cert") } // using our subject common name
|
||||
assertEquals(caCertAndKey.certificate.issuerDN, serverCert.issuerDN) // Issued by our CA cert
|
||||
serverCert.checkValidity(Date()) // throws on verification problems
|
||||
@ -107,7 +106,7 @@ class X509UtilitiesTest {
|
||||
val tmpKeyStore = tempFile("keystore.jks")
|
||||
val ecDSACert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"))
|
||||
val edDSAKeypair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||
val edDSACert = X509Utilities.createServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54"))
|
||||
val edDSACert = X509Utilities.createTlsServerCert(X500Name("CN=TestEdDSA"), edDSAKeypair.public, ecDSACert, listOf("alias name"), listOf("10.0.0.54"))
|
||||
|
||||
// Save the EdDSA private key with cert chains.
|
||||
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tmpKeyStore, "keystorepass")
|
||||
@ -177,14 +176,14 @@ class X509UtilitiesTest {
|
||||
|
||||
// Load signing intermediate CA cert
|
||||
val caKeyStore = KeyStoreUtilities.loadKeyStore(tmpCAKeyStore, "cakeystorepass")
|
||||
val caCertAndKey = caKeyStore.getCertificateAndKey(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass")
|
||||
val caCertAndKey = caKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_INTERMEDIATE_CA_PRIVATE_KEY, "cakeypass")
|
||||
|
||||
// Generate server cert and private key and populate another keystore suitable for SSL
|
||||
X509Utilities.createKeystoreForSSL(tmpServerKeyStore, "serverstorepass", "serverkeypass", caKeyStore, "cakeypass", MEGA_CORP.name)
|
||||
|
||||
// Load back server certificate
|
||||
val serverKeyStore = KeyStoreUtilities.loadKeyStore(tmpServerKeyStore, "serverstorepass")
|
||||
val serverCertAndKey = serverKeyStore.getCertificateAndKey(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass")
|
||||
val serverCertAndKey = serverKeyStore.getCertificateAndKeyPair(X509Utilities.CORDA_CLIENT_CA_PRIVATE_KEY, "serverkeypass")
|
||||
|
||||
serverCertAndKey.certificate.checkValidity(Date())
|
||||
serverCertAndKey.certificate.verify(caCertAndKey.certificate.publicKey)
|
||||
@ -349,4 +348,18 @@ class X509UtilitiesTest {
|
||||
|
||||
return keyStore
|
||||
}
|
||||
@Test
|
||||
fun `Get correct private key type from Keystore`() {
|
||||
val keyPair = Crypto.generateKeyPair(Crypto.ECDSA_SECP256R1_SHA256)
|
||||
val selfSignCert = X509Utilities.createSelfSignedCACert(X500Name("CN=Test"), keyPair)
|
||||
val keyStore = KeyStoreUtilities.loadOrCreateKeyStore(tempFile("testKeystore.jks"), "keystorepassword")
|
||||
keyStore.setKeyEntry("Key", keyPair.private, "keypassword".toCharArray(), arrayOf(selfSignCert.certificate))
|
||||
|
||||
val keyFromKeystore = keyStore.getKey("Key", "keypassword".toCharArray())
|
||||
val keyFromKeystoreCasted = keyStore.getSupportedKey("Key", "keypassword")
|
||||
|
||||
assertTrue(keyFromKeystore is java.security.interfaces.ECPrivateKey) // by default JKS returns SUN EC key
|
||||
assertTrue(keyFromKeystoreCasted is org.bouncycastle.jce.interfaces.ECPrivateKey)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,17 @@
|
||||
package net.corda.core.flows
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.contracts.asset.Cash
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.getOrThrow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.messaging.startFlow
|
||||
import net.corda.core.node.services.unconsumedStates
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.Emoji
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
@ -25,9 +28,7 @@ import net.corda.testing.startRpcClient
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFailsWith
|
||||
import kotlin.test.assertTrue
|
||||
@ -57,9 +58,8 @@ class ContractUpgradeFlowTest {
|
||||
fun `2 parties contract upgrade`() {
|
||||
// Create dummy contract.
|
||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
|
||||
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey)
|
||||
.signWith(b.services.legalIdentityKey)
|
||||
.toSignedTransaction()
|
||||
val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
|
||||
val stx = b.services.addSignature(signedByA)
|
||||
|
||||
a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity)))
|
||||
mockNet.runNetwork()
|
||||
@ -69,10 +69,10 @@ class ContractUpgradeFlowTest {
|
||||
requireNotNull(atx)
|
||||
requireNotNull(btx)
|
||||
|
||||
// The request is expected to be rejected because party B haven't authorise the upgrade yet.
|
||||
// The request is expected to be rejected because party B hasn't authorised the upgrade yet.
|
||||
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade.
|
||||
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<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
|
||||
mockNet.runNetwork()
|
||||
|
||||
val result = resultFuture.get()
|
||||
val result = resultFuture.getOrThrow()
|
||||
|
||||
fun check(node: MockNetwork.MockNode) {
|
||||
val nodeStx = node.database.transaction {
|
||||
@ -108,7 +108,7 @@ class ContractUpgradeFlowTest {
|
||||
rpcAddress = startRpcServer(
|
||||
rpcUser = user,
|
||||
ops = CordaRPCOpsImpl(node.services, node.smm, node.database)
|
||||
).get().hostAndPort,
|
||||
).get().broker.hostAndPort!!,
|
||||
username = user.username,
|
||||
password = user.password
|
||||
).get()
|
||||
@ -119,17 +119,16 @@ class ContractUpgradeFlowTest {
|
||||
rpcDriver {
|
||||
// Create dummy contract.
|
||||
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
|
||||
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey)
|
||||
.signWith(b.services.legalIdentityKey)
|
||||
.toSignedTransaction()
|
||||
val signedByA = a.services.signInitialTransaction(twoPartyDummyContract)
|
||||
val stx = b.services.addSignature(signedByA)
|
||||
|
||||
val user = rpcTestUser.copy(permissions = setOf(
|
||||
startFlowPermission<FinalityFlow>(),
|
||||
startFlowPermission<FinalityInvoker>(),
|
||||
startFlowPermission<ContractUpgradeFlow<*, *>>()
|
||||
))
|
||||
val rpcA = startProxy(a, user)
|
||||
val rpcB = startProxy(b, user)
|
||||
val handle = rpcA.startFlow(::FinalityFlow, stx, setOf(a.info.legalIdentity, b.info.legalIdentity))
|
||||
val handle = rpcA.startFlow(::FinalityInvoker, stx, setOf(a.info.legalIdentity, b.info.legalIdentity))
|
||||
mockNet.runNetwork()
|
||||
handle.returnValue.getOrThrow()
|
||||
|
||||
@ -143,7 +142,7 @@ class ContractUpgradeFlowTest {
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
mockNet.runNetwork()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
assertFailsWith(FlowSessionException::class) { rejectedFuture.getOrThrow() }
|
||||
|
||||
// Party B authorise the contract state upgrade.
|
||||
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||
@ -154,7 +153,7 @@ class ContractUpgradeFlowTest {
|
||||
DummyContractV2::class.java).returnValue
|
||||
|
||||
mockNet.runNetwork()
|
||||
val result = resultFuture.get()
|
||||
val result = resultFuture.getOrThrow()
|
||||
// Check results.
|
||||
listOf(a, b).forEach {
|
||||
val signedTX = a.database.transaction { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
||||
@ -186,21 +185,21 @@ class ContractUpgradeFlowTest {
|
||||
val firstState = a.database.transaction { a.vault.unconsumedStates<ContractState>().single() }
|
||||
assertTrue(firstState.state.data is CashV2.State, "Contract state is upgraded to the new version.")
|
||||
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (firstState.state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
|
||||
assertEquals(listOf(a.info.legalIdentity.owningKey), (firstState.state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
|
||||
assertEquals<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> {
|
||||
override val legacyContract = Cash::class.java
|
||||
|
||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<PublicKey>) : FungibleAsset<Currency> {
|
||||
override val owner: PublicKey = owners.first()
|
||||
override val exitKeys = (owners + amount.token.issuer.party.owningKey).toSet()
|
||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<AbstractParty>) : FungibleAsset<Currency> {
|
||||
override val owner: AbstractParty = owners.first()
|
||||
override val exitKeys = (owners + amount.token.issuer.party).map { it.owningKey }.toSet()
|
||||
override val contract = CashV2()
|
||||
override val participants = owners
|
||||
|
||||
override fun move(newAmount: Amount<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 withNewOwner(newOwner: PublicKey) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
|
||||
override fun withNewOwner(newOwner: AbstractParty) = Pair(Cash.Commands.Move(), copy(owners = listOf(newOwner)))
|
||||
}
|
||||
|
||||
override fun upgrade(state: Cash.State) = CashV2.State(state.amount.times(1000), listOf(state.owner))
|
||||
@ -210,4 +209,11 @@ class ContractUpgradeFlowTest {
|
||||
// Dummy Cash contract for testing.
|
||||
override val legalContractReference = SecureHash.sha256("")
|
||||
}
|
||||
|
||||
@StartableByRPC
|
||||
class FinalityInvoker(val transaction: SignedTransaction,
|
||||
val extraRecipients: Set<Party>) : FlowLogic<List<SignedTransaction>>() {
|
||||
@Suspendable
|
||||
override fun call(): List<SignedTransaction> = subFlow(FinalityFlow(transaction, extraRecipients))
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import net.corda.flows.ResolveTransactionsFlow
|
||||
import net.corda.node.utilities.transaction
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MEGA_CORP_KEY
|
||||
import net.corda.testing.MINI_CORP_PUBKEY
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
@ -94,7 +94,7 @@ class ResolveTransactionsFlowTest {
|
||||
val count = 50
|
||||
var cursor = stx2
|
||||
repeat(count) {
|
||||
val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP_PUBKEY)
|
||||
val stx = DummyContract.move(cursor.tx.outRef(0), MINI_CORP)
|
||||
.addSignatureUnchecked(NullSignature)
|
||||
.toSignedTransaction(false)
|
||||
a.database.transaction {
|
||||
@ -113,13 +113,13 @@ class ResolveTransactionsFlowTest {
|
||||
fun `triangle of transactions resolves fine`() {
|
||||
val stx1 = makeTransactions().first
|
||||
|
||||
val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP_PUBKEY).run {
|
||||
val stx2 = DummyContract.move(stx1.tx.outRef(0), MINI_CORP).run {
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
toSignedTransaction()
|
||||
}
|
||||
|
||||
val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP_PUBKEY).run {
|
||||
val stx3 = DummyContract.move(listOf(stx1.tx.outRef(0), stx2.tx.outRef(0)), MINI_CORP).run {
|
||||
signWith(MEGA_CORP_KEY)
|
||||
signWith(DUMMY_NOTARY_KEY)
|
||||
toSignedTransaction()
|
||||
@ -173,7 +173,7 @@ class ResolveTransactionsFlowTest {
|
||||
it.signWith(DUMMY_NOTARY_KEY)
|
||||
it.toSignedTransaction(false)
|
||||
}
|
||||
val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP_PUBKEY).let {
|
||||
val dummy2: SignedTransaction = DummyContract.move(dummy1.tx.outRef(0), MINI_CORP).let {
|
||||
it.signWith(MEGA_CORP_KEY)
|
||||
it.signWith(DUMMY_NOTARY_KEY)
|
||||
it.toSignedTransaction()
|
||||
|
@ -4,7 +4,6 @@ import net.corda.core.identity.Party
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.BOB
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.testing.MOCK_IDENTITY_SERVICE
|
||||
import net.corda.testing.node.MockNetwork
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@ -17,7 +16,6 @@ class TxKeyFlowUtilitiesTests {
|
||||
@Before
|
||||
fun before() {
|
||||
net = MockNetwork(false)
|
||||
net.identities += MOCK_IDENTITY_SERVICE.identities
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -5,6 +5,7 @@ import com.nhaarman.mockito_kotlin.mock
|
||||
import com.nhaarman.mockito_kotlin.whenever
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
import net.corda.core.node.services.StorageService
|
||||
@ -53,7 +54,7 @@ class AttachmentClassLoaderTests {
|
||||
class AttachmentDummyContract : Contract {
|
||||
data class State(val magicNumber: Int = 0) : ContractState {
|
||||
override val contract = ATTACHMENT_TEST_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf()
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,10 @@ package net.corda.core.node
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@ class VaultUpdateTests {
|
||||
}
|
||||
|
||||
private class DummyState : ContractState {
|
||||
override val participants: List<PublicKey>
|
||||
override val participants: List<AbstractParty>
|
||||
get() = emptyList()
|
||||
override val contract = VaultUpdateTests.DummyContract
|
||||
}
|
||||
|
@ -3,8 +3,11 @@ package net.corda.core.serialization
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import com.google.common.primitives.Ints
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.utilities.ALICE
|
||||
import net.corda.core.utilities.BOB
|
||||
import net.corda.node.services.messaging.Ack
|
||||
import net.corda.node.services.persistence.NodeAttachmentService
|
||||
import net.corda.testing.BOB_PUBKEY
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatThrownBy
|
||||
import org.junit.Before
|
||||
@ -12,6 +15,8 @@ import org.junit.Test
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.cert.CertPath
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
@ -136,6 +141,24 @@ class KryoTests {
|
||||
assertEquals(-1, readRubbishStream.read())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509Certififcate`() {
|
||||
val expected = X509Utilities.createSelfSignedCACert(ALICE.name).certificate
|
||||
val serialized = expected.serialize(kryo).bytes
|
||||
val actual: X509Certificate = serialized.deserialize(kryo)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `serialize - deserialize X509CertPath`() {
|
||||
val rootCA = X509Utilities.createSelfSignedCACert(ALICE.name)
|
||||
val certificate = X509Utilities.createTlsServerCert(BOB.name, BOB_PUBKEY, rootCA, emptyList(), emptyList())
|
||||
val expected = X509Utilities.createCertificatePath(rootCA, certificate, false).certPath
|
||||
val serialized = expected.serialize(kryo).bytes
|
||||
val actual: CertPath = serialized.deserialize(kryo)
|
||||
assertEquals(expected, actual)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
private data class Person(val name: String, val birthday: Instant?)
|
||||
|
||||
|
@ -2,14 +2,19 @@ package net.corda.core.serialization
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.utilities.*
|
||||
import net.corda.core.utilities.DUMMY_KEY_2
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.core.utilities.TEST_TX_TIME
|
||||
import net.corda.testing.MEGA_CORP
|
||||
import net.corda.testing.MEGA_CORP_KEY
|
||||
import net.corda.testing.MINI_CORP
|
||||
import net.corda.testing.generateStateRef
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import java.security.PublicKey
|
||||
import java.security.SignatureException
|
||||
import java.util.*
|
||||
import kotlin.test.assertEquals
|
||||
@ -27,12 +32,12 @@ class TransactionSerializationTests {
|
||||
data class State(
|
||||
val deposit: PartyAndReference,
|
||||
val amount: Amount<Currency>,
|
||||
override val owner: PublicKey) : OwnableState {
|
||||
override val owner: AbstractParty) : OwnableState {
|
||||
override val contract: Contract = TEST_PROGRAM_ID
|
||||
override val participants: List<PublicKey>
|
||||
override val participants: List<AbstractParty>
|
||||
get() = listOf(owner)
|
||||
|
||||
override fun withNewOwner(newOwner: PublicKey) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
override fun withNewOwner(newOwner: AbstractParty) = Pair(Commands.Move(), copy(owner = newOwner))
|
||||
}
|
||||
|
||||
interface Commands : CommandData {
|
||||
@ -44,9 +49,9 @@ class TransactionSerializationTests {
|
||||
// It refers to a fake TX/state that we don't bother creating here.
|
||||
val depositRef = MINI_CORP.ref(1)
|
||||
val fakeStateRef = generateStateRef()
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY), fakeStateRef)
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, DUMMY_PUBKEY_1), DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, DUMMY_KEY_1.public), DUMMY_NOTARY)
|
||||
val inputState = StateAndRef(TransactionState(TestCash.State(depositRef, 100.POUNDS, MEGA_CORP), DUMMY_NOTARY), fakeStateRef)
|
||||
val outputState = TransactionState(TestCash.State(depositRef, 600.POUNDS, MEGA_CORP), DUMMY_NOTARY)
|
||||
val changeState = TransactionState(TestCash.State(depositRef, 400.POUNDS, MEGA_CORP), DUMMY_NOTARY)
|
||||
|
||||
|
||||
lateinit var tx: TransactionBuilder
|
||||
@ -54,14 +59,14 @@ class TransactionSerializationTests {
|
||||
@Before
|
||||
fun setup() {
|
||||
tx = TransactionType.General.Builder(DUMMY_NOTARY).withItems(
|
||||
inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(DUMMY_KEY_1.public))
|
||||
inputState, outputState, changeState, Command(TestCash.Commands.Move(), arrayListOf(MEGA_CORP.owningKey))
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun signWireTX() {
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
tx.signWith(DUMMY_KEY_1)
|
||||
tx.signWith(MEGA_CORP_KEY)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
// Now check that the signature we just made verifies.
|
||||
@ -81,7 +86,7 @@ class TransactionSerializationTests {
|
||||
tx.toSignedTransaction()
|
||||
}
|
||||
|
||||
tx.signWith(DUMMY_KEY_1)
|
||||
tx.signWith(MEGA_CORP_KEY)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val signedTX = tx.toSignedTransaction()
|
||||
|
||||
@ -104,7 +109,7 @@ class TransactionSerializationTests {
|
||||
@Test
|
||||
fun timestamp() {
|
||||
tx.setTime(TEST_TX_TIME, 30.seconds)
|
||||
tx.signWith(DUMMY_KEY_1)
|
||||
tx.signWith(MEGA_CORP_KEY)
|
||||
tx.signWith(DUMMY_NOTARY_KEY)
|
||||
val stx = tx.toSignedTransaction()
|
||||
assertEquals(TEST_TX_TIME, stx.tx.timestamp?.midpoint)
|
||||
|
@ -71,7 +71,7 @@ class SecureHashGenerator : Generator<SecureHash>(SecureHash::class.java) {
|
||||
|
||||
class StateRefGenerator : Generator<StateRef>(StateRef::class.java) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): StateRef {
|
||||
return StateRef(SecureHash.Companion.sha256(random.nextBytes(16)), random.nextInt(0, 10))
|
||||
return StateRef(SecureHash.sha256(random.nextBytes(16)), random.nextInt(0, 10))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -11,4 +11,4 @@ else
|
||||
source virtualenv/Scripts/activate
|
||||
fi
|
||||
|
||||
make html
|
||||
make html
|
||||
|
BIN
docs/source/_static/corda-introductory-whitepaper-zhs.pdf
Normal file
BIN
docs/source/_static/corda-introductory-whitepaper-zhs.pdf
Normal file
Binary file not shown.
BIN
docs/source/_static/corda-introductory-whitepaper-zht.pdf
Normal file
BIN
docs/source/_static/corda-introductory-whitepaper-zht.pdf
Normal file
Binary file not shown.
@ -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 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.
|
||||
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/>`_.
|
||||
|
||||
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/>`_)
|
||||
|
||||
|
||||
Deploying the VM
|
||||
----------------
|
||||
Deploying the Corda Network
|
||||
---------------------------
|
||||
|
||||
Browse to portal.azure.com, login and search the Azure Marketplace for Corda and select 'Corda Single Ledger Network'.
|
||||
|
||||
Search the Azure Marketplace for Corda.
|
||||
Click the 'Create' button.
|
||||
|
||||
STEP 1: Basics
|
||||
|
||||
* **Name**: Choose an appropriate descriptive name for the VM
|
||||
* **VM Disk Type**: Select 'SSD'
|
||||
* **Username**: Your preferred user name for the administrator account when accessing via SSH
|
||||
* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box below. Alternatively select 'Password' to use a password of your choice to administer the VM
|
||||
Define the basic parameters which will be used to pre-configure your Corda nodes.
|
||||
|
||||
* **Subscription**: Select your subscription name
|
||||
* **Resource group**: Select 'Use existing'. From the drop-down menu, select your account group
|
||||
* **Resource prefix**: Choose an appropriate descriptive name for your Corda nodes. This name will prefix the node hostnames
|
||||
* **VM user name**: This is the user login name on the Ubuntu VMs. Leave it as azureuser or define your own
|
||||
* **Authentication type**: Select 'SSH public key', then paste the contents of your SSH public key file (see pre-requisites, above) into the box. Alternatively select 'Password' to use a password of your choice to administer the VM
|
||||
* **Restrict access by IP address**: Leave this as 'No' to allow access from any internet host, or provide an IP address or a range of IP addresses to limit access
|
||||
* **Subscription**: Select which of your Azure subscriptions you want to use
|
||||
* **Resource group**: Choose to 'Create new' and provide a useful name of your choice
|
||||
* **Location**: Select the geographical location physically closest to you
|
||||
|
||||
.. image:: resources/azure_vm_10_00_1.png
|
||||
.. image:: resources/azure_multi_node_step1.png
|
||||
:width: 300px
|
||||
|
||||
Click 'OK'
|
||||
|
||||
STEP 2: Size
|
||||
STEP 2: Network Size and Performance
|
||||
|
||||
A range of available hardware configurations will be presented, along with estimated costs. For the purposes of running the demos, a configuration of 2 cores and at least 14GB is recommended
|
||||
Define the number of Corda nodes in your network and the size of VM.
|
||||
|
||||
.. image:: resources/azure_vm_10_05_1.png
|
||||
* **Number of Network Map nodes**: There can only be one Network Map node in this network. Leave as '1'
|
||||
* **Number of Notary nodes**: There can only be one Notary node in this network. Leave as '1'
|
||||
* **Number of participant nodes**: This is the number of Corda nodes in your network. At least 2 nodes in your network is recommended (so you can send transactions between them). You can specific 1 participant node and use the Notary node as a second node. There is an upper limit of 9
|
||||
* **Storage performance**: Leave as 'Standard'
|
||||
* **Virtual machine size**: The size of the VM is automatically adjusted to suit the number of participant nodes selected. It is recommended to use the suggested values
|
||||
|
||||
.. image:: resources/azure_multi_node_step2.png
|
||||
:width: 300px
|
||||
|
||||
Choose the required configuration and click 'Select'.
|
||||
Click 'OK'
|
||||
|
||||
STEP 3: Settings
|
||||
STEP 3: Corda Specific Options
|
||||
|
||||
Adjust any configuration settings required. For the purposes of running the Corda demos, all settings may be left as default.
|
||||
Define the version of Corda you want on your nodes and the type of notary.
|
||||
|
||||
.. image:: resources/azure_vm_10_16_1.png
|
||||
* **Corda version (as seen in Maven Central)**: Select the version of Corda you want your nodes to use from the drop down list. The version numbers can be seen in `Maven Central <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
|
||||
|
||||
Click 'OK'
|
||||
|
||||
STEP 4: Summary
|
||||
|
||||
The banner at the top of the dialog should read 'Validation passed' otherwise go back and adjust settings where needed.
|
||||
A summary of your selections is shown.
|
||||
|
||||
.. image:: resources/azure_vm_10_19.png
|
||||
.. image:: resources/azure_multi_node_step4.png
|
||||
:width: 300px
|
||||
|
||||
Click 'OK' to proceed.
|
||||
Click 'OK' for your selection to be validated. If everything is ok you will see the message 'Validation passed'
|
||||
|
||||
Click 'OK'
|
||||
|
||||
STEP 5: Buy
|
||||
|
||||
Click 'Purchase' to complete the configuration and start the VM deployment.
|
||||
Review the Azure Terms of Use and Privacy Policy and click 'Purchase' to buy the Azure VMs which will host your Corda nodes.
|
||||
|
||||
The VM will begin the deployment process, which typically takes 4-5 minutes to complete. To see progress, click on the "Deploying" icon displayed.
|
||||
The deployment process will start and typically takes 8-10 minutes to complete.
|
||||
|
||||
.. image:: resources/azure_vm_10_20.png
|
||||
Once deployed click 'Resources Groups', select the resource group you defined in Step 1 above and click 'Overview' to see the virtual machine details. The names of your VMs will be pre-fixed with the resource prefix value you defined in Step 1 above.
|
||||
|
||||
The Newtork Map Service node is suffixed nm0. The Notary node is suffixed not0. Your Corda participant nodes are suffixed node0, node1, node2 etc. Note down the **Public IP address** for your Corda nodes. You will need these to connect to UI screens via your web browser:
|
||||
|
||||
.. image:: resources/azure_ip.png
|
||||
:width: 300px
|
||||
|
||||
Once deployed, click 'Overview' to see the virtual machine details. Note down the **Public IP address**. You will need this to connect to the demo screens via your web browser:
|
||||
Using the Yo! CorDapp
|
||||
---------------------
|
||||
Loading the Yo! CordDapp on your Corda nodes lets you send simple Yo! messages to other Corda nodes on the network. A Yo! message is a very simple transaction. The Yo! CorDapp demonstrates:
|
||||
|
||||
.. image:: resources/azure_vm_10_26.png
|
||||
:width: 300px
|
||||
- how transactions are only sent between Corda nodes which they are intended for and are not shared across the entire network by using the network map
|
||||
- uses a pre-defined flow to orchestrate the ledger update automatically
|
||||
- the contract imposes rules on the ledger updates
|
||||
|
||||
|
||||
Viewing the SIMM Valuation demo
|
||||
-------------------------------
|
||||
The SIMM Valuation demo creates three nodes, representing three parties in the example workflow (Bank A, Bank B, Bank C). Each node listens on a different port - those used by the demo are:
|
||||
* **Loading the Yo! CorDapp onto your nodes**
|
||||
|
||||
**SIMM Valuation Demo ports:** **12005 (node A for Bank A)**, **12007 (node B for Bank B)**, **12009 (node C for Bank C)**
|
||||
The nodes you will use to send and receive Yo messages require the Yo! CorDapp jar file to be saved to their plugins directory.
|
||||
|
||||
Open three browser tabs and direct each one to
|
||||
Connect to one of your Corda nodes (make sure this is not the Notary node) using an SSH client of your choice (e.g. Putty) and log into the virtual machine using the public IP address and your SSH key or username / password combination you defined in Step 1 of the Azure build process. Type the following command:
|
||||
|
||||
For Corda nodes running release M10
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://(public IP address):(port)/web/simmvaluationdemo
|
||||
cd /opt/corda/plugins
|
||||
wget http://downloads.corda.net/cordapps/net/corda/yo/0.10.1/yo-0.10.1.jar
|
||||
|
||||
specifying each of the three ports above in different windows, e.g.
|
||||
For Corda nodes running release M11
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://51.140.41.48/12005/web/simmvaluationdemo
|
||||
cd /opt/corda/plugins
|
||||
wget http://downloads.corda.net/cordapps/net/corda/yo/0.11.0/yo-0.11.0.jar
|
||||
|
||||
You will be able to view the basic web interface identifying the different banks.
|
||||
Now restart Corda and the Corda webserver using the following commands or restart your Corda VM from the Azure portal:
|
||||
|
||||
Now let's take a look at a transaction between Bank A and B which is not visible to Bank C. This illustrates the restricted data sharing feature of Corda, i.e. data is shared on a need-to-know basis. Nodes provide the dependency graph of a transaction they are sending to another node on demand, but there is no global broadcast of all transactions.
|
||||
.. sourcecode:: shell
|
||||
|
||||
1. In the browser tab for Bank A (the top right hand corner shows which bank you are administering) click 'Create New Trade' from the top navigation bar
|
||||
2. Select to trade with Bank B
|
||||
3. Select 'EUR Fixed 1y EURIBOR 3m' from the drop down
|
||||
4. Click 'Submit' to create the trade
|
||||
5. In the browser tab for Bank B click 'View Portfolio' from the top navigation bar to see this new trade
|
||||
6. In the browser tab for Bank C click 'View Portfolio' from the top navigation bar and you will not be able to see the trade, as expected
|
||||
sudo systemctl restart corda
|
||||
sudo systemctl restart corda-webserver
|
||||
|
||||
.. image:: resources/azure_vm_10_51.png
|
||||
Repeat these steps on other Corda nodes on your network which you want to send or receive Yo messages.
|
||||
|
||||
* **Verify the Yo! CorDapp is running**
|
||||
|
||||
Open a browser tab and browse to the following URL:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://(public IP address):(port)/web/yo
|
||||
|
||||
where (public IP address) is the public IP address of one of your Corda nodes on the Azure Corda network and (port) is the web server port number for your Corda node, 10004 by default
|
||||
|
||||
You will now see the Yo! CordDapp web interface:
|
||||
|
||||
.. image:: resources/Yo_web_ui.png
|
||||
:width: 300px
|
||||
|
||||
.. note:: There is a known issue whereby some users may see a 400 error when navigating the SIMM Valuation demo. If you encounter this error, simply navigate back to the root page (http://*(public IP address)*:*(port)*/*web*/*simmvaluationdemo*) in the browser before continuing.
|
||||
* **Sending a Yo message via the web interface**
|
||||
|
||||
Viewing the IRS demo
|
||||
--------------------
|
||||
The IRS demo creates three nodes: Bank A, Bank B and a node that runs a notary, a network map and an interest rates oracle together. The two banks agree on an interest rate swap, and then do regular fixings of the deal as the time on a simulated clock passes. Each bank node listens on a different port - those used by the demo are:
|
||||
|
||||
**IRS demo ports:** **11005 (node A for Bank A)**, **11007 (node B for Bank B)**
|
||||
|
||||
Open two browser tabs and direct one to each of the following:
|
||||
In the browser window type the following URL to send a Yo message to a target node on your Corda network:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://localhost:11005/web/irsdemo
|
||||
http://localhost:11007/web/irsdemo
|
||||
http://(public IP address):(port)/api/yo/yo?target=(legalname of target node)
|
||||
|
||||
You will be able to see the nodes' view of the ledger.
|
||||
where (public IP address) is the public IP address of one of your Corda nodes on the Azure Corda network and (port) is the web server port number for your Corda node, 10004 by default and (legalname of target node) is the Legal Name for the target node as defined in the node.conf file, for example:
|
||||
|
||||
.. image:: resources/azure_vm_10_52.png
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://40.69.40.42:10004/api/yo/yo?target=Corda 0.10.1 Node 1 in tstyo2
|
||||
|
||||
An easy way to see the Legal Names of Corda nodes on the network is to use the peers screen:
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://(public IP address):(port)/api/yo/peers
|
||||
|
||||
.. image:: resources/yo_peers2.png
|
||||
:width: 300px
|
||||
|
||||
Now let's take a look at how the interest rates oracle provides interest rates for a deal with a semi-annual payment frequency, and how the two counterparties to the trade see the same deal information on their own nodes, i.e. you see what I see.
|
||||
* **Viewing Yo messages**
|
||||
|
||||
1. In the browser tab for Bank A click 'Create Deal' from the top navigation bar
|
||||
2. Modify the terms of the IRS deal, or leave as default
|
||||
3. Click 'Submit' to create the deal
|
||||
4. In the browser tab for Bank A click 'Recent Deals' from the top navigation bar to view the deal
|
||||
5. In the browser tab for Bank B click 'Recent Deals' from the top navigation bar to view the deal. Compare the economic details to those shown in the Bank A tab
|
||||
To see Yo! messages sent to a particular node open a browser window and browse to the following URL:
|
||||
|
||||
.. image:: resources/azure_vm_10_54.png
|
||||
.. sourcecode:: shell
|
||||
|
||||
http://(public IP address):(port)/api/yo/yos
|
||||
|
||||
.. image:: resources/azure_yos.png
|
||||
:width: 300px
|
||||
|
||||
|
||||
Viewing logs (advanced users)
|
||||
-----------------------------
|
||||
Viewing logs
|
||||
------------
|
||||
Users may wish to view the raw logs generated by each node, which contain more information about the operations performed by each node.
|
||||
|
||||
You can access these using an SSH client of your choice (e.g. Putty) and logging into the virtual machine using the public IP address.
|
||||
Once logged in, navigate to
|
||||
Once logged in, navigate to the following directory for Corda logs (node-xxxxxx):
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
/opt/simm-nodes/
|
||||
/opt/corda/logs
|
||||
|
||||
for the SIMM Valuation demo logs and
|
||||
And navigate to the following directory for system logs (syslog):
|
||||
|
||||
.. sourcecode:: shell
|
||||
|
||||
/opt/irs-nodes/
|
||||
|
||||
for the IRS demo logs.
|
||||
There are separate sub-directories for each of the three nodes (*nodea*, *nodeb*, *nodec*), each containing a */logs* sub-directory.
|
||||
|
||||
The name of the log file will follow the name given to the service it reflects, e.g. *node-clint-vm-test.log*.
|
||||
|
||||
.. image:: resources/azure_vm_10_47.png
|
||||
:width: 300px
|
||||
/var/log
|
||||
|
||||
You can open log files with any text editor.
|
||||
|
||||
.. image:: resources/azure_vm_10_49.png
|
||||
:width: 300px
|
||||
|
||||
.. image:: resources/azure_syslog.png
|
||||
:width: 300px
|
||||
|
||||
Next Steps
|
||||
----------
|
||||
Now you have taken a look at two Corda demos do go and visit the `dedicated Corda website <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>`_
|
||||
|
@ -8,8 +8,11 @@ UNRELEASED
|
||||
----------
|
||||
|
||||
* API changes:
|
||||
* Initiating flows (i.e. those which initiate flows in a counterparty) are now required to be annotated with
|
||||
``InitiatingFlow``.
|
||||
* ``CordaPluginRegistry.requiredFlows`` is no longer needed. Instead annotate any flows you wish to start via RPC with
|
||||
``@StartableByRPC`` and any scheduled flows with ``@SchedulableFlow``.
|
||||
|
||||
* Flows which initiate flows in their counterparties (an example of which is the ``NotaryFlow.Client``) are now
|
||||
required to be annotated with ``@InitiatingFlow``.
|
||||
|
||||
* ``PluginServiceHub.registerFlowInitiator`` has been deprecated and replaced by ``registerServiceFlow`` with the
|
||||
marker Class restricted to ``FlowLogic``. In line with the introduction of ``InitiatingFlow``, it throws an
|
||||
@ -29,25 +32,55 @@ UNRELEASED
|
||||
* ``FlowLogic.getCounterpartyMarker`` is no longer used and been deprecated for removal. If you were using this to
|
||||
manage multiple independent message streams with the same party in the same flow then use sub-flows instead.
|
||||
|
||||
|
||||
* There are major changes to the ``Party`` class as part of confidential identities:
|
||||
|
||||
* ``Party`` has moved to the ``net.corda.core.identity`` package; there is a deprecated class in its place for
|
||||
backwards compatibility, but it will be removed in a future release and developers should move to the new class as soon
|
||||
as possible.
|
||||
* There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. A new class
|
||||
``AnonymousParty`` has been added, which is intended to be used in place of ``Party`` or ``PublicKey`` in contract
|
||||
state objects. The exception to this is where the party in a contract state is intended to be well known, such as
|
||||
issuer of a ``Cash`` state.
|
||||
* There is a new ``AbstractParty`` superclass to ``Party``, which contains just the public key. This now replaces
|
||||
use of ``Party`` and ``PublicKey`` in state objects, and allows use of full or anonymised parties depending on
|
||||
use-case.
|
||||
* Names of parties are now stored as a ``X500Name`` rather than a ``String``, to correctly enforce basic structure of the
|
||||
name. As a result all node legal names must now be structured as X.500 distinguished names.
|
||||
|
||||
* There are major changes to transaction signing in flows:
|
||||
|
||||
* You should use the new ``CollectSignaturesFlow`` and corresponding ``SignTransactionFlow`` which handle most
|
||||
of the details of this for you. They may get more complex in future as signing becomes a more featureful
|
||||
operation.
|
||||
* ``ServiceHub.legalIdentityKey`` no longer returns a ``KeyPair``, it instead returns just the ``PublicKey`` portion of this pair.
|
||||
The ``ServiceHub.notaryIdentityKey`` has changed similarly. The goal of this change is to keep private keys
|
||||
encapsulated and away from most flow code/Java code, so that the private key material can be stored in HSMs
|
||||
and other key management devices.
|
||||
* The ``KeyManagementService`` now provides no mechanism to request the node's ``PrivateKey`` objects directly.
|
||||
Instead signature creation occurs in the ``KeyManagementService.sign``, with the ``PublicKey`` used to indicate
|
||||
which of the node's multiple keys to use. This lookup also works for ``CompositeKey`` scenarios
|
||||
and the service will search for a leaf key hosted on the node.
|
||||
* The ``KeyManagementService.freshKey`` method now returns only the ``PublicKey`` portion of the newly generated ``KeyPair``
|
||||
with the ``PrivateKey`` kept internally to the service.
|
||||
* Flows which used to acquire a node's ``KeyPair``, typically via ``ServiceHub.legalIdentityKey``,
|
||||
should instead use the helper methods on ``ServiceHub``. In particular to freeze a ``TransactionBuilder`` and
|
||||
generate an initial partially signed ``SignedTransaction`` the flow should use ``ServiceHub.signInitialTransaction``.
|
||||
Flows generating additional party signatures should use ``ServiceHub.createSignature``. Each of these methods is
|
||||
provided with two signatures. One version that signs with the default node key, the other which allows key selection
|
||||
by passing in the ``PublicKey`` partner of the desired signing key.
|
||||
* The original ``KeyPair`` signing methods have been left on the ``TransactionBuilder`` and ``SignedTransaction``, but
|
||||
should only be used as part of unit testing.
|
||||
|
||||
* The ``InitiatingFlow`` annotation also has an integer ``version`` property which assigns the initiating flow a version
|
||||
number, defaulting to 1 if it's specified. The flow version is included in the flow session request and the counterparty
|
||||
will only respond and start their own flow if the version number matches to the one they've registered with. At some
|
||||
point we will support the ability for a node to have multiple versions of the same flow registered, enabling backwards
|
||||
compatibility of CorDapp flows.
|
||||
|
||||
Milestone 11.1
|
||||
--------------
|
||||
|
||||
* Fix serialisation error when starting a flow.
|
||||
* Automatically whitelist subclasses of `InputStream` when serialising.
|
||||
* Fix exception in DemoBench on Windows when loading CorDapps into the Node Explorer.
|
||||
* Detect when localhost resolution is broken on MacOSX, and provide instructions on how to fix it.
|
||||
|
||||
Milestone 11.0
|
||||
--------------
|
||||
|
||||
|
@ -6,14 +6,15 @@ compatible language the easiest way to do so is using the client library. The li
|
||||
node using a message queue protocol and then provides a simple RPC interface to interact with it. You make calls
|
||||
on a Java object as normal, and the marshalling back and forth is handled for you.
|
||||
|
||||
The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``proxy`` method that
|
||||
returns an implementation of the `CordaRPCOps`_ interface. A timeout parameter can be specified, and observables that
|
||||
are returned by RPCs can be subscribed to in order to receive an ongoing stream of updates from the node. More
|
||||
detail on how to use this is provided in the docs for the proxy method.
|
||||
The starting point for the client library is the `CordaRPCClient`_ class. This provides a ``start`` method that
|
||||
returns a `CordaRPCConnection`_, holding an implementation of the `CordaRPCOps`_ that may be accessed with ``proxy``
|
||||
in Kotlin and ``getProxy()`` in Java. Observables that are returned by RPCs can be subscribed to in order to receive
|
||||
an ongoing stream of updates from the node. More detail on how to use this is provided in the docs for the proxy method.
|
||||
|
||||
.. warning:: The returned object is somewhat expensive to create and consumes a small amount of server side
|
||||
resources. When you're done with it, cast it to ``Closeable`` or ``AutoCloseable`` and close it. Don't create
|
||||
one for every call you make - create a proxy and reuse it.
|
||||
.. warning:: The returned `CordaRPCConnection`_ is somewhat expensive to create and consumes a small amount of
|
||||
server side resources. When you're done with it, call ``close`` on it. Alternatively you may use the ``use``
|
||||
method on `CordaRPCClient`_ which cleans up automatically after the passed in lambda finishes. Don't create
|
||||
a new proxy for every call you make - reuse an existing one.
|
||||
|
||||
For a brief tutorial on how one can use the RPC API see :doc:`tutorial-clientrpc-api`.
|
||||
|
||||
@ -34,25 +35,21 @@ The returned observable may even emit object graphs with even more observables i
|
||||
would expect.
|
||||
|
||||
This feature comes with a cost: the server must queue up objects emitted by the server-side observable until you
|
||||
download them. Therefore RPCs that use this feature are marked with the ``@RPCReturnsObservables`` annotation, and
|
||||
you are expected to subscribe to all the observables returned. If you don't want an observable then subscribe
|
||||
then unsubscribe immediately to clear the buffers and indicate that you aren't interested. If your app quits then
|
||||
server side resources will be freed automatically.
|
||||
download them. Note that the server side observation buffer is bounded, once it fills up the client is considered
|
||||
slow and kicked. You are expected to subscribe to all the observables returned, otherwise client-side memory starts
|
||||
filling up as observations come in. If you don't want an observable then subscribe then unsubscribe immediately to
|
||||
clear the client-side buffers and to stop the server from streaming. If your app quits then server side resources
|
||||
will be freed automatically.
|
||||
|
||||
When all the observables returned by an RPC are unsubscribed on the client side, that unsubscription propagates
|
||||
through to the server where the corresponding server-side observables are also unsubscribed.
|
||||
|
||||
.. warning:: If you leak an observable or proxy on the client side and it gets garbage collected, you will get
|
||||
a warning printed to the logs and the proxy will be closed for you. But don't rely on this, as garbage
|
||||
collection is non-deterministic.
|
||||
.. warning:: If you leak an observable on the client side and it gets garbage collected, you will get a warning
|
||||
printed to the logs and the observable will be unsubscribed for you. But don't rely on this, as garbage collection
|
||||
is non-deterministic.
|
||||
|
||||
Futures
|
||||
-------
|
||||
|
||||
A method can also return a ``ListenableFuture`` in its object graph and it will be treated in a similar manner to
|
||||
observables, including needing to mark the RPC with the ``@RPCReturnsObservables`` annotation. Unlike for an observable,
|
||||
once the single value (or an exception) has been received all server-side resources will be released automatically. Calling
|
||||
the ``cancel`` method on the future will unsubscribe it from any future value and release any resources.
|
||||
observables. Calling the ``cancel`` method on the future will unsubscribe it from any future value and release any resources.
|
||||
|
||||
Versioning
|
||||
----------
|
||||
@ -66,13 +63,9 @@ of, an ``UnsupportedOperationException`` is thrown. If you want to know the vers
|
||||
Thread safety
|
||||
-------------
|
||||
|
||||
A proxy is thread safe, blocking, and will only allow a single RPC to be in flight at once. Any observables that
|
||||
are returned and you subscribe to will have objects emitted on a background thread. Observables returned as part
|
||||
of one RPC and observables returned from another may have their callbacks invoked in parallel, but observables
|
||||
returned as part of the same specific RPC invocation are processed serially and will not be invoked in parallel.
|
||||
|
||||
If you want to make multiple calls to the server in parallel you can do that by creating multiple proxies, but
|
||||
be aware that the server itself may *not* process your work in parallel even if you make your requests that way.
|
||||
A proxy is thread safe, blocking, and allows multiple RPCs to be in flight at once. Any observables that are returned and
|
||||
you subscribe to will have objects emitted in order on a background thread pool. Each Observable stream is tied to a single
|
||||
thread, however note that two separate Observables may invoke their respective callbacks on different threads.
|
||||
|
||||
Error handling
|
||||
--------------
|
||||
@ -85,8 +78,7 @@ side as if it was thrown from inside the called RPC method. These exceptions can
|
||||
Wire protocol
|
||||
-------------
|
||||
|
||||
The client RPC wire protocol is not currently documented. To use it you must use the client library provided.
|
||||
This is likely to change in a future release.
|
||||
The client RPC wire protocol is defined and documented in ``net/corda/client/rpc/RPCApi.kt``.
|
||||
|
||||
Whitelisting classes with the Corda node
|
||||
----------------------------------------
|
||||
@ -98,5 +90,6 @@ with the annotation ``@CordaSerializable``. See :doc:`creating-a-cordapp` or :d
|
||||
|
||||
.. warning:: We will be replacing the use of Kryo in the serialization framework and so additional changes here are likely.
|
||||
|
||||
.. _CordaRPCClient: api/kotlin/corda/net.corda.client.rpc/-corda-r-p-c-client/index.html
|
||||
.. _CordaRPCOps: api/kotlin/corda/net.corda.core.messaging/-corda-r-p-c-ops/index.html
|
||||
.. _CordaRPCClient: api/javadoc/net/corda/client/rpc/CordaRPCClient.html
|
||||
.. _CordaRPCOps: api/javadoc/net/corda/core/messaging/CordaRPCOps.html
|
||||
.. _CordaRPCConnection: api/javadoc/net/corda/client/rpc/CordaRPCConnection.html
|
||||
|
@ -81,7 +81,8 @@ path to the node's base directory.
|
||||
|
||||
.. note:: In practice the ArtemisMQ messaging services bind to all local addresses on the specified port. However,
|
||||
note that the host is the included as the advertised entry in the NetworkMapService. As a result the value listed
|
||||
here must be externally accessible when running nodes across a cluster of machines.
|
||||
here must be externally accessible when running nodes across a cluster of machines. If the provided host is unreachable,
|
||||
the node will try to auto-discover its public one.
|
||||
|
||||
:rpcAddress: The address of the RPC system on which RPC requests can be made to the node. If not provided then the node will run without RPC.
|
||||
|
||||
|
@ -45,19 +45,7 @@ extensions to be created, or registered at startup. In particular:
|
||||
jars. These static serving directories will not be available if the
|
||||
bundled web server is not started.
|
||||
|
||||
c. The ``requiredFlows`` property is used to declare new protocols in
|
||||
the plugin jar. Specifically the property must return a map with a key
|
||||
naming each exposed top level flow class and a value which is a set
|
||||
naming every parameter class that will be passed to the flow's
|
||||
constructor. Standard ``java.lang.*`` and ``kotlin.*`` types do not need
|
||||
to be included, but all other parameter types, or concrete interface
|
||||
implementations need declaring. Declaring a specific flow in this map
|
||||
white lists it for activation by the ``FlowLogicRefFactory``. White
|
||||
listing is not strictly required for ``subFlows`` used internally, but
|
||||
is required for any top level flow, or a flow which is invoked through
|
||||
the scheduler.
|
||||
|
||||
d. The ``servicePlugins`` property returns a list of classes which will
|
||||
c. The ``servicePlugins`` property returns a list of classes which will
|
||||
be instantiated once during the ``AbstractNode.start`` call. These
|
||||
classes must provide a single argument constructor which will receive a
|
||||
``PluginServiceHub`` reference. They must also extend the abstract class
|
||||
@ -90,7 +78,7 @@ extensions to be created, or registered at startup. In particular:
|
||||
functions inside the node, for instance to initiate workflows when
|
||||
certain conditions are met.
|
||||
|
||||
e. The ``customizeSerialization`` function allows classes to be whitelisted
|
||||
d. The ``customizeSerialization`` function allows classes to be whitelisted
|
||||
for object serialisation, over and above those tagged with the ``@CordaSerializable``
|
||||
annotation. In general the annotation should be preferred. For
|
||||
instance new state types will need to be explicitly registered. This will be called at
|
||||
|
@ -12,11 +12,10 @@ App plugins
|
||||
To create an app plugin you must extend from `CordaPluginRegistry`_. The JavaDoc contains
|
||||
specific details of the implementation, but you can extend the server in the following ways:
|
||||
|
||||
1. Required flows: Specify which flows will be whitelisted for use in your RPC calls.
|
||||
2. Service plugins: Register your services (see below).
|
||||
3. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
||||
4. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
||||
5. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
||||
1. Service plugins: Register your services (see below).
|
||||
2. Web APIs: You may register your own endpoints under /api/ of the bundled web server.
|
||||
3. Static web endpoints: You may register your own static serving directories for serving web content from the web server.
|
||||
4. Whitelisting your additional contract, state and other classes for object serialization. Any class that forms part
|
||||
of a persisted state, that is used in messaging between flows or in RPC needs to be whitelisted.
|
||||
|
||||
Services
|
||||
|
@ -42,7 +42,8 @@ There are two main steps to implementing scheduled events:
|
||||
``nextScheduledActivity`` to be implemented which returns an optional ``ScheduledActivity`` instance.
|
||||
``ScheduledActivity`` captures what ``FlowLogic`` instance each node will run, to perform the activity, and when it
|
||||
will run is described by a ``java.time.Instant``. Once your state implements this interface and is tracked by the
|
||||
wallet, it can expect to be queried for the next activity when committed to the wallet.
|
||||
wallet, it can expect to be queried for the next activity when committed to the wallet. The ``FlowLogic`` must be
|
||||
annotated with ``@SchedulableFlow``.
|
||||
* If nothing suitable exists, implement a ``FlowLogic`` to be executed by each node as the activity itself.
|
||||
The important thing to remember is that in the current implementation, each node that is party to the transaction
|
||||
will execute the same ``FlowLogic``, so it needs to establish roles in the business process based on the contract
|
||||
@ -90,10 +91,7 @@ business process and to take on those roles. That ``FlowLogic`` will be handed
|
||||
rate swap ``State`` in question, as well as a tolerance ``Duration`` of how long to wait after the activity is triggered
|
||||
for the interest rate before indicating an error.
|
||||
|
||||
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to
|
||||
instantiate. The reference can be checked against a per-node whitelist of approved and allowable types as
|
||||
part of our overall security sandboxing.
|
||||
|
||||
.. note:: This is a way to create a reference to the FlowLogic class and its constructor parameters to instantiate.
|
||||
|
||||
As previously mentioned, we currently need a small network handler to assist with session setup until the work to
|
||||
automate that is complete. See the interest rate swap specific implementation ``FixingSessionInitiationHandler`` which
|
||||
|
@ -8,7 +8,6 @@ import net.corda.core.contracts.StateAndRef
|
||||
import net.corda.core.contracts.TransactionType
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.sign
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.Party
|
||||
@ -48,12 +47,12 @@ private fun gatherOurInputs(serviceHub: ServiceHub,
|
||||
notary: Party?): Pair<List<StateAndRef<Cash.State>>, Long> {
|
||||
// Collect cash type inputs
|
||||
val cashStates = serviceHub.vaultService.unconsumedStates<Cash.State>()
|
||||
// extract our key identity for convenience
|
||||
val ourKey = serviceHub.myInfo.legalIdentity.owningKey
|
||||
// extract our identity for convenience
|
||||
val ourIdentity = serviceHub.myInfo.legalIdentity
|
||||
// Filter down to our own cash states with right currency and issuer
|
||||
val suitableCashStates = cashStates.filter {
|
||||
val state = it.state.data
|
||||
(state.owner == ourKey)
|
||||
(state.owner == ourIdentity)
|
||||
&& (state.amount.token == amountRequired.token)
|
||||
}
|
||||
require(!suitableCashStates.isEmpty()) { "Insufficient funds" }
|
||||
@ -90,12 +89,12 @@ private fun prepareOurInputsAndOutputs(serviceHub: ServiceHub, request: FxReques
|
||||
val (inputs, residual) = gatherOurInputs(serviceHub, sellAmount, request.notary)
|
||||
|
||||
// Build and an output state for the counterparty
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty.owningKey)
|
||||
val transferedFundsOutput = Cash.State(sellAmount, request.counterparty)
|
||||
|
||||
if (residual > 0L) {
|
||||
// Build an output state for the residual change back to us
|
||||
val residualAmount = Amount(residual, sellAmount.token)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity.owningKey)
|
||||
val residualOutput = Cash.State(residualAmount, serviceHub.myInfo.legalIdentity)
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput, residualOutput))
|
||||
} else {
|
||||
return FxResponse(inputs, listOf(transferedFundsOutput))
|
||||
@ -140,7 +139,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
require(it.inputs.all { it.state.notary == notary }) {
|
||||
"notary of remote states must be same as for our states"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner.owningKey }) {
|
||||
require(it.inputs.all { it.state.data.owner == remoteRequestWithNotary.owner }) {
|
||||
"The inputs are not owned by the correct counterparty"
|
||||
}
|
||||
require(it.inputs.all { it.state.data.amount.token == remoteRequestWithNotary.amount.token }) {
|
||||
@ -153,7 +152,7 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
>= remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided inputs don't provide sufficient funds"
|
||||
}
|
||||
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity.owningKey }.
|
||||
require(it.outputs.filter { it.owner == serviceHub.myInfo.legalIdentity }.
|
||||
map { it.amount.quantity }.sum() == remoteRequestWithNotary.amount.quantity) {
|
||||
"the provided outputs don't provide the request quantity"
|
||||
}
|
||||
@ -195,8 +194,8 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
val builder = TransactionType.General.Builder(ourStates.inputs.first().state.notary)
|
||||
|
||||
// Add the move commands and key to indicate all the respective owners and need to sign
|
||||
val ourSigners = ourStates.inputs.map { it.state.data.owner }.toSet()
|
||||
val theirSigners = theirStates.inputs.map { it.state.data.owner }.toSet()
|
||||
val ourSigners = ourStates.inputs.map { it.state.data.owner.owningKey }.toSet()
|
||||
val theirSigners = theirStates.inputs.map { it.state.data.owner.owningKey }.toSet()
|
||||
builder.addCommand(Cash.Commands.Move(), (ourSigners + theirSigners).toList())
|
||||
|
||||
// Build and add the inputs and outputs
|
||||
@ -206,11 +205,9 @@ class ForeignExchangeFlow(val tradeId: String,
|
||||
builder.withItems(*theirStates.outputs.toTypedArray())
|
||||
|
||||
// We have already validated their response and trust our own data
|
||||
// so we can sign
|
||||
builder.signWith(serviceHub.legalIdentityKey)
|
||||
// create a signed transaction, but pass false as parameter, because we know it is not fully signed
|
||||
val signedTransaction = builder.toSignedTransaction(checkSufficientSignatures = false)
|
||||
return signedTransaction
|
||||
// so we can sign. Note the returned SignedTransaction is still not fully signed
|
||||
// and would not pass full verification yet.
|
||||
return serviceHub.signInitialTransaction(builder)
|
||||
}
|
||||
// DOCEND 3
|
||||
}
|
||||
@ -260,7 +257,7 @@ class ForeignExchangeRemoteFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
}
|
||||
|
||||
// assuming we have completed state and business level validation we can sign the trade
|
||||
val ourSignature = serviceHub.legalIdentityKey.sign(proposedTrade.id)
|
||||
val ourSignature = serviceHub.createSignature(proposedTrade)
|
||||
|
||||
// send the other side our signature.
|
||||
send(source, ourSignature)
|
||||
|
@ -2,9 +2,12 @@ package net.corda.docs
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.crypto.containsAny
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.flows.InitiatingFlow
|
||||
import net.corda.core.identity.AbstractParty
|
||||
import net.corda.core.identity.Party
|
||||
import net.corda.core.node.PluginServiceHub
|
||||
import net.corda.core.node.ServiceHub
|
||||
@ -64,10 +67,10 @@ data class TradeApprovalContract(override val legalContractReference: SecureHash
|
||||
override val contract: TradeApprovalContract = TradeApprovalContract()) : LinearState {
|
||||
|
||||
val parties: List<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 {
|
||||
return participants.any { it.containsAny(ourKeys) }
|
||||
return participants.any { it.owningKey.containsAny(ourKeys) }
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,9 +134,7 @@ class SubmitTradeApprovalFlow(val tradeId: String,
|
||||
.withItems(tradeProposal, Command(TradeApprovalContract.Commands.Issue(), listOf(tradeProposal.source.owningKey)))
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can automatically sign as there is no untrusted data.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to a SignedTransaction that we can send to the notary
|
||||
val signedTx = tx.toSignedTransaction(false)
|
||||
val signedTx = serviceHub.signInitialTransaction(tx)
|
||||
// Notarise and distribute.
|
||||
subFlow(FinalityFlow(signedTx, setOf(serviceHub.myInfo.legalIdentity, counterparty)))
|
||||
// Return the initial state
|
||||
@ -195,9 +196,9 @@ class SubmitCompletionFlow(val ref: StateRef, val verdict: WorkflowState) : Flow
|
||||
tx.setTime(serviceHub.clock.instant(), Duration.ofSeconds(60))
|
||||
// We can sign this transaction immediately as we have already checked all the fields and the decision
|
||||
// is ultimately a manual one from the caller.
|
||||
tx.signWith(serviceHub.legalIdentityKey)
|
||||
// Convert to SignedTransaction we can pass around certain that it cannot be modified.
|
||||
val selfSignedTx = tx.toSignedTransaction(false)
|
||||
// As a SignedTransaction we can pass the data around certain that it cannot be modified,
|
||||
// although we do require further signatures to complete the process.
|
||||
val selfSignedTx = serviceHub.signInitialTransaction(tx)
|
||||
//DOCEND 2
|
||||
// Send the signed transaction to the originator and await their signature to confirm
|
||||
val allPartySignedTx = sendAndReceive<DigitalSignature.WithKey>(newState.source, selfSignedTx).unwrap {
|
||||
@ -253,7 +254,7 @@ class RecordCompletionFlow(val source: Party) : FlowLogic<Unit>() {
|
||||
}
|
||||
// DOCEND 3
|
||||
// Having verified the SignedTransaction passed to us we can sign it too
|
||||
val ourSignature = serviceHub.legalIdentityKey.sign(completeTx.tx.id)
|
||||
val ourSignature = serviceHub.createSignature(completeTx)
|
||||
// Send our signature to the other party.
|
||||
send(source, ourSignature)
|
||||
// N.B. The FinalityProtocol will be responsible for Notarising the SignedTransaction
|
||||
|
@ -8,7 +8,6 @@ import net.corda.core.toFuture
|
||||
import net.corda.core.utilities.DUMMY_NOTARY
|
||||
import net.corda.core.utilities.DUMMY_NOTARY_KEY
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.CashPaymentFlow
|
||||
import net.corda.node.services.network.NetworkMapService
|
||||
import net.corda.node.services.transactions.ValidatingNotaryService
|
||||
import net.corda.node.utilities.transaction
|
||||
|
63
docs/source/flow-library.rst
Normal file
63
docs/source/flow-library.rst
Normal 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.
|
@ -131,7 +131,7 @@ each side.
|
||||
val notaryNode: NodeInfo,
|
||||
val assetToSell: StateAndRef<OwnableState>,
|
||||
val price: Amount<Currency>,
|
||||
val myKeyPair: KeyPair,
|
||||
val myKey: PublicKey,
|
||||
override val progressTracker: ProgressTracker = Seller.tracker()) : FlowLogic<SignedTransaction>() {
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
@ -160,7 +160,8 @@ Going through the data needed to become a seller, we have:
|
||||
information on notaries.
|
||||
- ``assetToSell: StateAndRef<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).
|
||||
- ``myKeyPair: KeyPair`` - the key pair that controls the asset being sold. It will be used to sign the transaction.
|
||||
- ``myKey: PublicKey`` - the PublicKey part of the node's internal KeyPair that controls the asset being sold.
|
||||
The matching PrivateKey stored in the KeyManagementService will be used to sign the transaction.
|
||||
|
||||
And for the buyer:
|
||||
|
||||
@ -206,9 +207,10 @@ how to register handlers with the messaging system (see ":doc:`messaging`") and
|
||||
when messages arrive. It provides the send/receive/sendAndReceive calls that let the code request network
|
||||
interaction and it will save/restore serialised versions of the fiber at the right times.
|
||||
|
||||
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events,
|
||||
see ":doc:`event-scheduling`" to learn more about this. Or they can be triggered directly via the Java-level node RPC
|
||||
APIs from your app code.
|
||||
Flows can be invoked in several ways. For instance, they can be triggered by scheduled events (in which case they need to
|
||||
be annotated with ``@SchedulableFlow``), see ":doc:`event-scheduling`" to learn more about this. They can also be triggered
|
||||
directly via the node's RPC API from your app code (in which case they need to be annotated with `StartableByRPC`). It's
|
||||
possible for a flow to be of both types.
|
||||
|
||||
You request a flow to be invoked by using the ``CordaRPCOps.startFlowDynamic`` method. This takes a
|
||||
Java reflection ``Class`` object that describes the flow class to use (in this case, either ``Buyer`` or ``Seller``).
|
||||
@ -399,15 +401,35 @@ This code is longer but no more complicated. Here are some things to pay attenti
|
||||
As you can see, the flow logic is straightforward and does not contain any callbacks or network glue code, despite
|
||||
the fact that it takes minimal resources and can survive node restarts.
|
||||
|
||||
Initiating communication
|
||||
------------------------
|
||||
Flow sessions
|
||||
-------------
|
||||
|
||||
Now that we have both sides of the deal negotation implemented as flows we need a way to start things off. We do this by
|
||||
having one side initiate communication and the other respond to it and start their flow. Initiation is typically done using
|
||||
RPC with the ``startFlowDynamic`` method. The initiating flow has be to annotated with ``InitiatingFlow``. In our example
|
||||
it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor ``Seller``
|
||||
are annotated with it. For example, if we choose the seller side as the initiator then we need a seller starter flow that
|
||||
might look something like this:
|
||||
Before going any further it will be useful to describe how flows communicate with each other. A node may have many flows
|
||||
running at the same time, and perhaps communicating with the same counterparty node but for different purposes. Therefore
|
||||
flows need a way to segregate communication channels so that concurrent conversations between flows on the same set of nodes
|
||||
do not interfere with each other.
|
||||
|
||||
To achieve this the flow framework initiates a new flow session each time a flow starts communicating with a ``Party``
|
||||
for the first time. A session is simply a pair of IDs, one for each side, to allow the node to route received messages to
|
||||
the correct flow. If the other side accepts the session request then subsequent sends and receives to that same ``Party``
|
||||
will use the same session. A session ends when either flow ends, whether as expected or pre-maturely. If a flow ends
|
||||
pre-maturely then the other side will be notified of that and they will also end, as the whole point of flows is a known
|
||||
sequence of message transfers. Flows end pre-maturely due to exceptions, and as described above, if that exception is
|
||||
``FlowException`` or a sub-type then it will propagate to the other side. Any other exception will not propagate.
|
||||
|
||||
Taking a step back, we mentioned that the other side has to accept the session request for there to be a communication
|
||||
channel. A node accepts a session request if it has registered the flow type (the fully-qualified class name) that is
|
||||
making the request - each session initiation includes the initiating flow type. The registration is done by a CorDapp
|
||||
which has made available the particular flow communication, using ``PluginServiceHub.registerServiceFlow``. This method
|
||||
specifies a flow factory for generating the counter-flow to any given initiating flow. If this registration doesn't exist
|
||||
then no further communication takes place and the initiating flow ends with an exception. The initiating flow has to be
|
||||
annotated with ``InitiatingFlow``.
|
||||
|
||||
Going back to our buyer and seller flows, we need a way to initiate communication between the two. This is typically done
|
||||
with one side started manually using the ``startFlowDynamic`` RPC and this initiates the counter-flow on the other side.
|
||||
In this case it doesn't matter which flow is the initiator and which is the initiated, which is why neither ``Buyer`` nor
|
||||
``Seller`` are annotated with ``InitiatingFlow``. For example, if we choose the seller side as the initiator then we need
|
||||
to create a simple seller starter flow that has the annotation we need:
|
||||
|
||||
.. container:: codeset
|
||||
|
||||
@ -418,7 +440,7 @@ might look something like this:
|
||||
@Suspendable
|
||||
override fun call(): SignedTransaction {
|
||||
val notary: NodeInfo = serviceHub.networkMapCache.notaryNodes[0]
|
||||
val cpOwnerKey: KeyPair = serviceHub.legalIdentityKey
|
||||
val cpOwnerKey: PublicKey = serviceHub.legalIdentityKey
|
||||
return subFlow(TwoPartyTradeFlow.Seller(otherParty, notary, assetToSell, price, cpOwnerKey))
|
||||
}
|
||||
}
|
||||
|
@ -4,63 +4,142 @@ Getting set up
|
||||
Software requirements
|
||||
---------------------
|
||||
|
||||
Corda uses industry-standard tools to make set-up as simple as possible. Following the software recommendations below will
|
||||
minimize the number of errors you encounter, and make it easier for others to provide support. However, if you do use other tools,
|
||||
we're interested to hear about any issues that arise.
|
||||
Corda uses industry-standard tools to make set-up as simple as possible. Following the software recommendations below will minimize the number of errors you encounter, and make it easier for others to provide support. However, if you do use other tools, we'd be interested to hear about any issues that arise.
|
||||
|
||||
JVM
|
||||
~~~
|
||||
|
||||
Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively
|
||||
supported. Oracle JDK 8 can be obtained directly from
|
||||
`Oracle <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>`_.
|
||||
Corda is written in Kotlin and runs in a JVM. We develop against Oracle JDK 8, and other JVM implementations are not actively supported.
|
||||
|
||||
Please ensure that you keep your Oracle JDK installation updated to the latest version while working with Corda.
|
||||
Even earlier versions of JDK 8 versions can cause cryptic errors.
|
||||
Please ensure that you keep your Oracle JDK installation updated to the latest version while working with Corda. Even earlier versions of JDK 8 versions can cause cryptic errors.
|
||||
|
||||
If you do choose to use OpenJDK instead of Oracle's JDK, you will also need to install OpenJFX.
|
||||
|
||||
Additional troubleshooting information can be found `here <https://docs.corda.net/getting-set-up-fault-finding.html#java-issues>`_.
|
||||
|
||||
Kotlin
|
||||
~~~~~~
|
||||
|
||||
Applications on Corda (CorDapps) can be written in any JVM-targeting language. However, Corda itself and most of the samples
|
||||
are written in Kotlin. If you're unfamiliar with Kotlin, there is an official `getting started guide <https://kotlinlang.org/docs/tutorials/>`_.
|
||||
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/>`_.
|
||||
|
||||
See also our :doc:`further-notes-on-kotlin`.
|
||||
|
||||
IDE
|
||||
~~~
|
||||
|
||||
We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration. The free Community
|
||||
Edition can be downloaded from `JetBrains <https://www.jetbrains.com/idea/download/>`_.
|
||||
We strongly recommend the use of IntelliJ IDEA as an IDE, primarily due to the strength of its Kotlin integration.
|
||||
|
||||
Please make sure that you're running the latest version of IDEA, as older versions have been known to have problems integrating with Gradle,
|
||||
the build tool used by Corda.
|
||||
|
||||
You'll also want to install the Kotlin IDEA plugin by following the instructions
|
||||
`here <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>`_.
|
||||
Please make sure that you're running the latest version of IDEA, as older versions have been known to have problems integrating with Gradle, the build tool used by Corda.
|
||||
|
||||
Git
|
||||
~~~
|
||||
|
||||
We use git to version-control Corda. Instructions on installing git can be found
|
||||
`here <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>`_.
|
||||
We use git to version-control Corda.
|
||||
|
||||
Gradle
|
||||
~~~~~~
|
||||
|
||||
We use Gradle as the build tool for Corda. However, you do not need to install Gradle itself, as a wrapper is provided.
|
||||
|
||||
The wrapper can be run from the command line by using ``./gradlew [taskName]`` on OS X/Linux, or ``gradlew.bat [taskName]`` on Windows.
|
||||
Set-up instructions
|
||||
-------------------
|
||||
|
||||
The instructions below will allow you to set up a Corda development environment and run a basic CorDapp on a Windows or Mac machine. If you have any issues, please consult the :doc:`getting-set-up-fault-finding` page, or reach out on `Slack <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
|
||||
-----------------
|
||||
@ -79,25 +158,7 @@ And a simple example CorDapp for you to explore basic concepts is available here
|
||||
|
||||
You can clone these repos to your local machine by running the command ``git clone [repo URL]``.
|
||||
|
||||
By default, these repos will be on the ``master`` branch. However, this is an unstable development branch. You should check
|
||||
out the latest release tag instead by running ``git checkout release-M10.1``.
|
||||
|
||||
Opening Corda/CorDapps in IDEA
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. warning:: If you choose to use IntelliJ you must run the ``gradlew kaptKotlin`` task before attempting to compile via IntelliJ.
|
||||
|
||||
.. note:: If you change branch , gradle clean or see a compile error in ``VaultSchemaTest.kt`` you must also then re-run `gradlew kaptKotlin`
|
||||
|
||||
When opening a Corda project for the first time from the IDEA splash screen, please click "Open" rather than "Import Project",
|
||||
and then import the Gradle project by clicking "Import Gradle project" in the popup bubble on the lower right-hand side of the screen.
|
||||
If you instead pick "Import Project" on the splash screen, a bug in IDEA will cause Corda's pre-packaged run configurations to be erased.
|
||||
|
||||
If you see this warning too late, that's not a problem - just use ``git checkout .idea/runConfigurations`` or the version control tab in
|
||||
IDEA to undelete the files.
|
||||
|
||||
IDEA's build of the project may need to be resynced from time to time. This can be done from within IDEA by going to "View" -> "Tool Windows" -> "Gradle"
|
||||
and clicking "Refresh all Gradle projects". Whenever prompted about Gradle, accept the defaults suggested by IDEA.
|
||||
By default, these repos will be on the unstable ``master`` branch. You should check out the latest milestone release instead by running ``git checkout release-M11.1``.
|
||||
|
||||
Next steps
|
||||
----------
|
||||
|
@ -2,10 +2,10 @@ Welcome to the Corda documentation!
|
||||
===================================
|
||||
|
||||
.. warning:: This build of the docs is from the "|version|" branch, not a milestone release. It may not reflect the
|
||||
current state of the code. `Read the docs for milestone release M10.1 <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)
|
||||
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/>`_.
|
||||
|
||||
If you're new to Corda, you should start by learning about its motivating vision and architecture. A good introduction
|
||||
@ -118,6 +118,7 @@ Documentation Contents:
|
||||
:maxdepth: 2
|
||||
:caption: Component library
|
||||
|
||||
flow-library
|
||||
contract-catalogue
|
||||
contract-irs
|
||||
|
||||
|
@ -55,10 +55,9 @@ resolving the attachment references to the attachments. Commands with valid sign
|
||||
When constructing a new transaction from scratch, you use ``TransactionBuilder``, which is a mutable transaction that
|
||||
can be signed once its construction is complete. This builder class should be used to create the initial transaction representation
|
||||
(before signature, before verification). It is intended to be passed around code that may edit it by adding new states/commands.
|
||||
Then once the states and commands are right, this class can be used as a holding bucket to gather signatures from multiple parties.
|
||||
It is typical for contract classes to expose helper methods that can contribute to a ``TransactionBuilder``. Once a transaction
|
||||
has been constructed using the builders ``toWireTransaction`` or ``toSignedTransaction`` function, it shared with other
|
||||
participants using the :doc:`key-concepts-flow-framework`.
|
||||
Then once the states and commands are right then an initial DigitalSignature.WithKey can be added to freeze the transaction data.
|
||||
Typically, the signInitialTransaction method on the flow's serviceHub object will be used to look up the default node identity PrivateKey,
|
||||
sign the transaction and return a partially signed SignedTransaction. This can then be distributed to other participants using the :doc:`key-concepts-flow-framework`.
|
||||
|
||||
Here's an example of building a transaction that creates an issuance of bananas (note that bananas are not a real
|
||||
contract type in the library):
|
||||
@ -69,10 +68,9 @@ contract type in the library):
|
||||
|
||||
val notaryToUse: Party = ...
|
||||
val txb = TransactionBuilder(notary = notaryToUse).withItems(BananaState(Amount(20, Bananas), fromCountry = "Elbonia"))
|
||||
txb.signWith(myKey)
|
||||
txb.setTime(Instant.now(), notaryToUse, 30.seconds)
|
||||
// We must disable the check for sufficient signatures, because this transaction is not yet notarised.
|
||||
val stx = txb.toSignedTransaction(checkSufficientSignatures = false)
|
||||
// Carry out the initial signing of the transaction and creation of a (partial) SignedTransation.
|
||||
val stx = serviceHub.signInitialTransaction(txb)
|
||||
// Alternatively, let's just check it verifies pretending it was fully signed. To do this, we get
|
||||
// a WireTransaction, which is what the SignedTransaction wraps. Thus by verifying that directly we
|
||||
// skip signature checking.
|
||||
|
@ -1,21 +1,14 @@
|
||||
Overview
|
||||
========
|
||||
|
||||
This section describes the fundamental concepts and features that underpin the Corda platform, to include:
|
||||
This section describes the key concepts and features of the Corda platform.
|
||||
|
||||
* :doc:`key-concepts-ecosystem`
|
||||
* :doc:`key-concepts-data-model`
|
||||
* :doc:`key-concepts-core-types`
|
||||
* :doc:`key-concepts-financial-model`
|
||||
* :doc:`key-concepts-flow-framework`
|
||||
* :doc:`key-concepts-consensus-notaries`
|
||||
* :doc:`key-concepts-vault`
|
||||
* :doc:`key-concepts-security-model`
|
||||
|
||||
Detailed thinking and rationale behind these concepts are presented in the following published white papers:
|
||||
The detailed thinking and rationale behind these concepts are presented in two white papers:
|
||||
|
||||
* `Corda: An Introduction`_
|
||||
* `Corda: A Distributed Ledger`_ (Technical White Paper)
|
||||
* `Corda: A Distributed Ledger`_ (A.K.A. the Technical White Paper)
|
||||
|
||||
Explanations of the key concepts are also available as `videos <https://vimeo.com/album/4555732/>`_.
|
||||
|
||||
.. _`Corda: An Introduction`: _static/corda-introductory-whitepaper.pdf
|
||||
.. _`Corda: A Distributed Ledger`: _static/corda-technical-whitepaper.pdf
|
||||
|
@ -65,12 +65,12 @@ PersistentKeyManagementService and E2ETestKeyManagementService
|
||||
Typical usage of these services is to locate an appropriate
|
||||
``PrivateKey`` to complete and sign a verified transaction as part of a
|
||||
flow. The normal node legal identifier keys are typically accessed via
|
||||
helper extension methods on the ``ServiceHub``, but these ultimately
|
||||
fetch the keys from the ``KeyManagementService``. The
|
||||
helper extension methods on the ``ServiceHub``, but these ultimately delegate
|
||||
signing to internal ``PrivateKeys`` from the ``KeyManagementService``. The
|
||||
``KeyManagementService`` interface also allows other keys to be
|
||||
generated if anonymous keys are needed in a flow. Note that this
|
||||
interface works at the level of individual ``PublicKey``/``PrivateKey``
|
||||
pairs, but the signing authority will be represented by a
|
||||
interface works at the level of individual ``PublicKey`` and internally
|
||||
matched ``PrivateKey` pairs, but the signing authority may be represented by a
|
||||
``CompositeKey`` on the ``NodeInfo`` to allow key clustering and
|
||||
threshold schemes.
|
||||
|
||||
|
@ -168,11 +168,12 @@ Let's see what parameters we pass to the constructor of this oracle.
|
||||
|
||||
.. sourcecode:: kotlin
|
||||
|
||||
class Oracle(val identity: Party, private val signingKey: KeyPair, val clock: Clock) = TODO()
|
||||
class Oracle(val identity: Party, private val signingKey: PublicKey, val clock: Clock) = TODO()
|
||||
|
||||
Here we see the oracle needs to have its own identity, so it can check which transaction commands it is expected to
|
||||
sign for, and also needs a pair of signing keys with which it signs transactions. The clock is used for the deadline
|
||||
functionality which we will not discuss further here.
|
||||
sign for, and also needs the PublicKey portion of its signing key. Later this PublicKey will be passed to the KeyManagementService
|
||||
to identify the internal PrivateKey used for transaction signing.
|
||||
The clock is used for the deadline functionality which we will not discuss further here.
|
||||
|
||||
Assuming you have a data source and can query it, it should be very easy to implement your ``query`` method and the
|
||||
parameter and ``CommandData`` classes.
|
||||
|
@ -10,7 +10,8 @@ We've added the ability for flows to be versioned by their CorDapp developers. T
|
||||
version of a flow and allows it to reject flow communication with a node which isn't using the same fact. In a future
|
||||
release we allow a node to have multiple versions of the same flow running to enable backwards compatibility.
|
||||
|
||||
There are major changes to the ``Party`` class as part of confidential identities. See :doc:`changelog` for full details.
|
||||
There are major changes to the ``Party`` class as part of confidential identities, and how parties and keys are stored
|
||||
in transaction state objects. See :doc:`changelog` for full details.
|
||||
|
||||
|
||||
Milestone 11
|
||||
|
@ -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.
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user