Merge branch 'master' into mike-enterprise-merge-june-20th

This commit is contained in:
Mike Hearn 2017-06-21 19:49:33 +02:00
commit ca8f5050cf
559 changed files with 13444 additions and 6160 deletions

2
.gitignore vendored
View File

@ -84,7 +84,7 @@ crashlytics-build.properties
docs/virtualenv/
# bft-smart
config/currentView
**/config/currentView
# vim
*.swp

6
.idea/compiler.xml generated
View File

@ -38,6 +38,8 @@
<module name="explorer_test" target="1.8" />
<module name="finance_main" target="1.8" />
<module name="finance_test" target="1.8" />
<module name="intellij-plugin_main" target="1.8" />
<module name="intellij-plugin_test" target="1.8" />
<module name="irs-demo_integrationTest" target="1.8" />
<module name="irs-demo_main" target="1.8" />
<module name="irs-demo_test" target="1.8" />
@ -62,6 +64,7 @@
<module name="node-schemas_test" target="1.8" />
<module name="node_integrationTest" target="1.8" />
<module name="node_main" target="1.8" />
<module name="node_smokeTest" target="1.8" />
<module name="node_test" target="1.8" />
<module name="notary-demo_main" target="1.8" />
<module name="notary-demo_test" target="1.8" />
@ -86,6 +89,9 @@
<module name="simm-valuation-demo_integrationTest" target="1.8" />
<module name="simm-valuation-demo_main" target="1.8" />
<module name="simm-valuation-demo_test" target="1.8" />
<module name="smoke-test-utils_main" target="1.8" />
<module name="smoke-test-utils_test" target="1.8" />
<module name="test-utils_integrationTest" target="1.8" />
<module name="test-utils_main" target="1.8" />
<module name="test-utils_test" target="1.8" />
<module name="tools_main" target="1.8" />

View File

@ -1,5 +1,7 @@
![Corda](https://www.corda.net/wp-content/uploads/2016/11/fg005_corda_b.png)
<a href="https://ci-master.corda.r3cev.com/viewType.html?buildTypeId=Corda_CordaBuild&tab=buildTypeStatusDiv&guest=1"><img src="https://ci.corda.r3cev.com/app/rest/builds/buildType:Corda_CordaBuild/statusIcon"/></a>
# Corda
Corda is a decentralised database system in which nodes trust each other as little as possible.
@ -12,8 +14,6 @@ Corda is a decentralised database system in which nodes trust each other as litt
* "Notary" infrastructure to validate uniqueness of transactions
* Written as a platform for distributed apps called CorDapps
* Written in [Kotlin](https://kotlinlang.org), targeting the JVM
Read our full and planned feature list [here](https://docs.corda.net/inthebox.html).
## Getting started
@ -29,7 +29,7 @@ After the above, watching the following webinars will give you a great introduct
### Webinar 1 [Introduction to Corda](https://vimeo.com/192757743/c2ec39c1e1)
Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session.
Richard Brown, R3 Chief Technology Officer, explains Corda's unique architecture, the only distributed ledger platform designed by and for the financial industry's unique requirements. You may want to read the [Corda non-technical whitepaper](https://www.r3cev.com/s/corda-introductory-whitepaper-final.pdf) as pre-reading for this session.
### Webinar 2 [Corda Developers Tutorial](https://vimeo.com/192797322/aab499b152)

View File

@ -5,7 +5,7 @@ buildscript {
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// Our version: bump this on release.
ext.corda_release_version = "0.12-SNAPSHOT"
ext.corda_release_version = "0.13-SNAPSHOT"
// Increment this on any release that changes public APIs anywhere in the Corda platform
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
ext.corda_platform_version = 1
@ -34,7 +34,7 @@ buildscript {
ext.guava_version = constants.getProperty("guavaVersion")
ext.quickcheck_version = '0.7'
ext.okhttp_version = '3.5.0'
ext.netty_version = '4.1.5.Final'
ext.netty_version = '4.1.9.Final'
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
ext.fileupload_version = '1.3.2'
ext.junit_version = '4.12'
@ -45,7 +45,7 @@ buildscript {
ext.h2_version = '1.4.194'
ext.rxjava_version = '1.2.4'
ext.requery_version = '1.2.1'
ext.dokka_version = '0.9.13'
ext.dokka_version = '0.9.14'
ext.eddsa_version = '0.2.0'
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
@ -219,17 +219,15 @@ tasks.withType(Test) {
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
directory "./build/nodes"
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB"
node {
name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
name "CN=Controller,O=R3,OU=corda,L=London,C=GB"
advertisedServices = ["corda.notary.validating"]
p2pPort 10002
cordapps = []
}
node {
name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
nearestCity "London"
name "CN=Bank A,O=R3,OU=corda,L=London,C=GB"
advertisedServices = []
p2pPort 10012
rpcPort 10013
@ -237,8 +235,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
cordapps = []
}
node {
name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
nearestCity "New York"
name "CN=Bank B,O=R3,OU=corda,L=London,C=GB"
advertisedServices = []
p2pPort 10007
rpcPort 10008
@ -257,7 +254,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
license {
name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0'

View File

@ -4,6 +4,7 @@ apply plugin: 'net.corda.plugins.publish-utils'
dependencies {
compile project(':core')
compile project(':finance')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre8:$kotlin_version"
testCompile "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
@ -23,3 +24,11 @@ dependencies {
testCompile "com.pholser:junit-quickcheck-core:$quickcheck_version"
testCompile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
}
jar {
baseName 'corda-jackson'
}
publish {
name = jar.baseName
}

View File

@ -7,8 +7,8 @@ import com.fasterxml.jackson.databind.deser.std.StringArrayDeserializer
import com.fasterxml.jackson.databind.module.SimpleModule
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount
import net.corda.core.contracts.BusinessCalendar
import net.corda.core.crypto.*
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
@ -20,10 +20,10 @@ import net.corda.core.serialization.OpaqueBytes
import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigDecimal
import java.security.PublicKey
import java.time.LocalDate
import java.util.*
/**
@ -39,29 +39,33 @@ object JacksonSupport {
interface PartyObjectMapper {
@Deprecated("Use partyFromX500Name instead")
fun partyFromName(partyName: String): Party?
fun partyFromPrincipal(principal: X500Name): Party?
fun partyFromX500Name(name: X500Name): Party?
fun partyFromKey(owningKey: PublicKey): Party?
fun partiesFromName(query: String): Set<Party>
}
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
class RpcObjectMapper(val rpc: CordaRPCOps, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
override fun partyFromPrincipal(principal: X500Name): Party? = rpc.partyFromX500Name(principal)
override fun partyFromX500Name(name: X500Name): Party? = rpc.partyFromX500Name(name)
override fun partyFromKey(owningKey: PublicKey): Party? = rpc.partyFromKey(owningKey)
override fun partiesFromName(query: String) = rpc.partiesFromName(query, fuzzyIdentityMatch)
}
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
class IdentityObjectMapper(val identityService: IdentityService, factory: JsonFactory, val fuzzyIdentityMatch: Boolean) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
override fun partyFromPrincipal(principal: X500Name): Party? = identityService.partyFromX500Name(principal)
override fun partyFromX500Name(name: X500Name): Party? = identityService.partyFromX500Name(name)
override fun partyFromKey(owningKey: PublicKey): Party? = identityService.partyFromKey(owningKey)
override fun partiesFromName(query: String) = identityService.partiesFromName(query, fuzzyIdentityMatch)
}
class NoPartyObjectMapper(factory: JsonFactory) : PartyObjectMapper, ObjectMapper(factory) {
@Suppress("OverridingDeprecatedMember", "DEPRECATION")
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
override fun partyFromPrincipal(principal: X500Name): Party? = throw UnsupportedOperationException()
override fun partyFromX500Name(name: X500Name): Party? = throw UnsupportedOperationException()
override fun partyFromKey(owningKey: PublicKey): Party? = throw UnsupportedOperationException()
override fun partiesFromName(query: String) = throw UnsupportedOperationException()
}
val cordaModule: Module by lazy {
@ -77,6 +81,7 @@ object JacksonSupport {
addSerializer(SecureHash.SHA256::class.java, SecureHashSerializer)
addDeserializer(SecureHash::class.java, SecureHashDeserializer())
addDeserializer(SecureHash.SHA256::class.java, SecureHashDeserializer())
addSerializer(BusinessCalendar::class.java, CalendarSerializer)
addDeserializer(BusinessCalendar::class.java, CalendarDeserializer)
// For ed25519 pubkeys
@ -106,17 +111,31 @@ object JacksonSupport {
}
}
/** Mapper requiring RPC support to deserialise parties from names */
/**
* Creates a Jackson ObjectMapper that uses RPC to deserialise parties from string names.
*
* If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely
* match an identity known from the network map. If true, the name is matched more leniently but if the match
* is ambiguous a [JsonParseException] is thrown.
*/
@JvmStatic @JvmOverloads
fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory))
fun createDefaultMapper(rpc: CordaRPCOps, factory: JsonFactory = JsonFactory(),
fuzzyIdentityMatch: Boolean = false): ObjectMapper = configureMapper(RpcObjectMapper(rpc, factory, fuzzyIdentityMatch))
/** For testing or situations where deserialising parties is not required */
@JvmStatic @JvmOverloads
fun createNonRpcMapper(factory: JsonFactory = JsonFactory()): ObjectMapper = configureMapper(NoPartyObjectMapper(factory))
/** For testing with an in memory identity service */
/**
* Creates a Jackson ObjectMapper that uses an [IdentityService] directly inside the node to deserialise parties from string names.
*
* If [fuzzyIdentityMatch] is false, fields mapped to [Party] objects must be in X.500 name form and precisely
* match an identity known from the network map. If true, the name is matched more leniently but if the match
* is ambiguous a [JsonParseException] is thrown.
*/
@JvmStatic @JvmOverloads
fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory()) = configureMapper(IdentityObjectMapper(identityService, factory))
fun createInMemoryMapper(identityService: IdentityService, factory: JsonFactory = JsonFactory(),
fuzzyIdentityMatch: Boolean = false) = configureMapper(IdentityObjectMapper(identityService, factory, fuzzyIdentityMatch))
private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
enable(SerializationFeature.INDENT_OUTPUT)
@ -170,14 +189,21 @@ object JacksonSupport {
// how to parse the content
return if (parser.text.contains("=")) {
val principal = X500Name(parser.text)
mapper.partyFromPrincipal(principal) ?: throw JsonParseException(parser, "Could not find a Party with name ${principal}")
mapper.partyFromX500Name(principal) ?: throw JsonParseException(parser, "Could not find a Party with name $principal")
} else {
val key = try {
parsePublicKeyBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Could not interpret ${parser.text} as a base58 encoded public key")
val nameMatches = mapper.partiesFromName(parser.text)
if (nameMatches.isEmpty()) {
val key = try {
parsePublicKeyBase58(parser.text)
} catch (e: Exception) {
throw JsonParseException(parser, "Could not find a matching party for '${parser.text}' and is not a base58 encoded public key")
}
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
} else if (nameMatches.size == 1) {
nameMatches.first()
} else {
throw JsonParseException(parser, "Ambiguous name match '${parser.text}': could be any of " + nameMatches.map { it.name }.joinToString(" ... or ..."))
}
mapper.partyFromKey(key) ?: throw JsonParseException(parser, "Could not find a Party with key ${key.toStringShort()}")
}
}
}
@ -244,11 +270,30 @@ object JacksonSupport {
}
}
data class BusinessCalendarWrapper(val holidayDates: List<LocalDate>) {
fun toCalendar() = BusinessCalendar(holidayDates)
}
object CalendarSerializer : JsonSerializer<BusinessCalendar>() {
override fun serialize(obj: BusinessCalendar, generator: JsonGenerator, context: SerializerProvider) {
val calendarName = BusinessCalendar.calendars.find { BusinessCalendar.getInstance(it) == obj }
if(calendarName != null) {
generator.writeString(calendarName)
} else {
generator.writeObject(BusinessCalendarWrapper(obj.holidayDates))
}
}
}
object CalendarDeserializer : JsonDeserializer<BusinessCalendar>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): BusinessCalendar {
return try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
try {
val array = StringArrayDeserializer.instance.deserialize(parser, context)
BusinessCalendar.getInstance(*array)
} catch (e: Exception) {
parser.readValueAs(BusinessCalendarWrapper::class.java).toCalendar()
}
} catch (e: Exception) {
throw JsonParseException(parser, "Invalid calendar(s) ${parser.text}: ${e.message}")
}

View File

@ -55,3 +55,11 @@ task integrationTest(type: Test) {
testClassesDir = sourceSets.integrationTest.output.classesDir
classpath = sourceSets.integrationTest.runtimeClasspath
}
jar {
baseName 'corda-jfx'
}
publish {
name = jar.baseName
}

View File

@ -28,11 +28,11 @@ import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
import net.corda.node.driver.driver
import net.corda.node.services.network.NetworkMapService
import net.corda.node.services.startFlowPermission
import net.corda.node.services.transactions.SimpleNotaryService
import net.corda.nodeapi.User
import net.corda.testing.driver.driver
import net.corda.testing.expect
import net.corda.testing.expectEvents
import net.corda.testing.node.DriverBasedTest
@ -123,14 +123,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
vaultUpdates.expectEvents(isStrict = false) {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.isEmpty()) { output.produced.size }
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.isEmpty()) { produced.size }
},
// ISSUE
expect { output: Vault.Update ->
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.size == 1) { output.produced.size }
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.size == 1) { produced.size }
}
)
}
@ -207,19 +207,19 @@ class NodeMonitorModelTest : DriverBasedTest() {
vaultUpdates.expectEvents {
sequence(
// SNAPSHOT
expect { output: Vault.Update ->
require(output.consumed.isEmpty()) { output.consumed.size }
require(output.produced.isEmpty()) { output.produced.size }
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.isEmpty()) { produced.size }
},
// ISSUE
expect { update ->
require(update.consumed.isEmpty()) { update.consumed.size }
require(update.produced.size == 1) { update.produced.size }
expect { (consumed, produced) ->
require(consumed.isEmpty()) { consumed.size }
require(produced.size == 1) { produced.size }
},
// MOVE
expect { update ->
require(update.consumed.size == 1) { update.consumed.size }
require(update.produced.isEmpty()) { update.produced.size }
expect { (consumed, produced) ->
require(consumed.size == 1) { consumed.size }
require(produced.isEmpty()) { produced.size }
}
)
}
@ -227,14 +227,14 @@ class NodeMonitorModelTest : DriverBasedTest() {
stateMachineTransactionMapping.expectEvents {
sequence(
// ISSUE
expect { mapping ->
require(mapping.stateMachineRunId == issueSmId)
require(mapping.transactionId == issueTx!!.id)
expect { (stateMachineRunId, transactionId) ->
require(stateMachineRunId == issueSmId)
require(transactionId == issueTx!!.id)
},
// MOVE
expect { mapping ->
require(mapping.stateMachineRunId == moveSmId)
require(mapping.transactionId == moveTx!!.id)
expect { (stateMachineRunId, transactionId) ->
require(stateMachineRunId == moveSmId)
require(transactionId == moveTx!!.id)
}
)
}

View File

@ -23,3 +23,11 @@ dependencies {
testCompile project(':test-utils')
}
jar {
baseName 'corda-mock'
}
publish {
name = jar.baseName
}

View File

@ -60,6 +60,7 @@ dependencies {
testCompile project(':client:mock')
// Smoke tests do NOT have any Node code on the classpath!
smokeTestCompile project(':smoke-test-utils')
smokeTestCompile project(':finance')
smokeTestCompile "org.apache.logging.log4j:log4j-slf4j-impl:$log4j_version"
smokeTestCompile "org.apache.logging.log4j:log4j-core:$log4j_version"
@ -76,5 +77,12 @@ task integrationTest(type: Test) {
task smokeTest(type: Test) {
testClassesDir = sourceSets.smokeTest.output.classesDir
classpath = sourceSets.smokeTest.runtimeClasspath
systemProperties['build.dir'] = buildDir
}
jar {
baseName 'corda-rpc'
}
publish {
name = jar.baseName
}

View File

@ -5,17 +5,19 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.base.Stopwatch
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.*
import net.corda.core.messaging.RPCOps
import net.corda.node.driver.poll
import net.corda.testing.driver.poll
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCKryo
import net.corda.testing.*
import org.apache.activemq.artemis.ArtemisConstants
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@ -24,12 +26,10 @@ import rx.Observable
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
import java.time.Duration
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
import kotlin.test.fail
class RPCStabilityTests {
@ -218,22 +218,65 @@ class RPCStabilityTests {
@Test
fun `client reconnects to rebooted server`() {
rpcDriver {
val ops = object : ReconnectOps {
override val protocolVersion = 0
override fun ping() = "pong"
// TODO: Remove multiple trials when we fix the Artemis bug (which should have its own test(s)).
if (ArtemisConstants::class.java.`package`.implementationVersion == "1.5.3") {
// The test fails maybe 1 in 100 times, so to stay green until we upgrade Artemis, retry if it fails:
for (i in (1..3)) {
try {
`client reconnects to rebooted server`(1)
} catch (e: TimeoutException) {
continue
}
return
}
fail("Test failed 3 times, which is vanishingly unlikely unless something has changed.")
} else {
// We've upgraded Artemis so make the test fail reliably, in the 2.1.0 case that takes 25 trials:
`client reconnects to rebooted server`(25)
}
}
private fun `client reconnects to rebooted server`(trials: Int) {
rpcDriver {
val coreBurner = thread {
while (!Thread.interrupted()) {
// Spin.
}
}
try {
val ops = object : ReconnectOps {
override val protocolVersion = 0
override fun ping() = "pong"
}
var serverFollower = shutdownManager.follower()
val serverPort = startRpcServer<ReconnectOps>(ops = ops).getOrThrow().broker.hostAndPort!!
serverFollower.unfollow()
val clientFollower = shutdownManager.follower()
val client = startRpcClient<ReconnectOps>(serverPort).getOrThrow()
clientFollower.unfollow()
assertEquals("pong", client.ping())
val background = Executors.newSingleThreadExecutor()
(1..trials).forEach {
System.err.println("Start trial $it of $trials.")
serverFollower.shutdown()
serverFollower = shutdownManager.follower()
startRpcServer<ReconnectOps>(ops = ops, customPort = serverPort).getOrThrow()
serverFollower.unfollow()
val stopwatch = Stopwatch.createStarted()
val pingFuture = background.submit(Callable {
client.ping() // Would also hang in foreground, we need it in background so we can timeout.
})
assertEquals("pong", pingFuture.getOrThrow(10.seconds))
System.err.println("Took ${stopwatch.elapsed(TimeUnit.MILLISECONDS)} millis.")
}
background.shutdown() // No point in the hanging case.
clientFollower.shutdown() // Driver would do this after the current server, causing 'legit' failover hang.
} finally {
with(coreBurner) {
interrupt()
join()
}
}
val serverFollower = shutdownManager.follower()
val serverPort = startRpcServer<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.
}
}

View File

@ -157,7 +157,7 @@ class RPCClientProxyHandler(
lifeCycle.requireState(State.UNSTARTED)
reaperExecutor = Executors.newScheduledThreadPool(
1,
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").build()
ThreadFactoryBuilder().setNameFormat("rpc-client-reaper-%d").setDaemon(true).build()
)
reaperScheduledFuture = reaperExecutor!!.scheduleAtFixedRate(
this::reapObservables,

View File

@ -1,42 +1,47 @@
package net.corda.kotlin.rpc
import java.io.FilterInputStream
import java.io.InputStream
import java.nio.file.Path
import java.nio.file.Paths
import java.time.Duration.ofSeconds
import java.util.Currency
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.*
import com.google.common.hash.Hashing
import com.google.common.hash.HashingInputStream
import net.corda.client.rpc.CordaRPCConnection
import net.corda.client.rpc.notUsed
import net.corda.core.contracts.*
import net.corda.core.contracts.DOLLARS
import net.corda.core.contracts.POUNDS
import net.corda.core.contracts.SWISS_FRANCS
import net.corda.core.crypto.SecureHash
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow
import net.corda.core.messaging.startTrackedFlow
import net.corda.core.seconds
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.sizedInputStreamAndHash
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor
import net.corda.flows.CashIssueFlow
import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import org.apache.commons.io.output.NullOutputStream
import org.junit.After
import org.junit.Before
import org.junit.Test
import java.io.FilterInputStream
import java.io.InputStream
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotEquals
class StandaloneCordaRPClientTest {
private companion object {
val log = loggerFor<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
val timeout = 60.seconds
}
private lateinit var notary: NodeProcess
@ -55,7 +60,7 @@ class StandaloneCordaRPClientTest {
@Before
fun setUp() {
notary = factory.create(notaryConfig)
notary = NodeProcess.Factory().create(notaryConfig)
connection = notary.connect()
rpcProxy = connection.proxy
notaryIdentity = fetchNotaryIdentity()
@ -71,17 +76,23 @@ class StandaloneCordaRPClientTest {
}
@Test
fun `test attachment upload`() {
fun `test attachments`() {
val attachment = sizedInputStreamAndHash(attachmentSize)
assertFalse(rpcProxy.attachmentExists(attachment.sha256))
val id = WrapperStream(attachment.inputStream).use { rpcProxy.uploadAttachment(it) }
assertEquals(id, attachment.sha256, "Attachment has incorrect SHA256 hash")
assertEquals(attachment.sha256, id, "Attachment has incorrect SHA256 hash")
val hash = HashingInputStream(Hashing.sha256(), rpcProxy.openAttachment(id)).use { it ->
it.copyTo(NullOutputStream())
SecureHash.SHA256(it.hash().asBytes())
}
assertEquals(attachment.sha256, hash)
}
@Test
fun `test starting flow`() {
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
.returnValue.getOrThrow(timeout)
}
@Test
@ -94,7 +105,7 @@ class StandaloneCordaRPClientTest {
log.info("Flow>> $msg")
++trackCount
}
handle.returnValue.getOrThrow(ofSeconds(timeout))
handle.returnValue.getOrThrow(timeout)
assertNotEquals(0, trackCount)
}
@ -118,7 +129,7 @@ class StandaloneCordaRPClientTest {
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
.returnValue.getOrThrow(timeout)
assertEquals(1, updateCount)
}
@ -135,7 +146,7 @@ class StandaloneCordaRPClientTest {
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(ofSeconds(timeout))
.returnValue.getOrThrow(timeout)
assertNotEquals(0, updateCount)
// Check that this cash exists in the vault

View File

@ -1,34 +1,27 @@
package net.corda.client.rpc
import com.codahale.metrics.ConsoleReporter
import com.codahale.metrics.Gauge
import com.codahale.metrics.JmxReporter
import com.codahale.metrics.MetricRegistry
import com.google.common.base.Stopwatch
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.RPCOps
import net.corda.core.minutes
import net.corda.core.seconds
import net.corda.core.utilities.Rate
import net.corda.core.utilities.div
import net.corda.node.driver.ShutdownManager
import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.RPCDriverExposedDSLInterface
import net.corda.testing.driver.ShutdownManager
import net.corda.testing.measure
import net.corda.testing.performance.startPublishingFixedRateInjector
import net.corda.testing.performance.startReporter
import net.corda.testing.performance.startTightLoopInjector
import net.corda.testing.rpcDriver
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import java.time.Duration
import java.util.*
import java.util.concurrent.*
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.locks.ReentrantLock
import javax.management.ObjectName
import kotlin.concurrent.thread
import kotlin.concurrent.withLock
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
@Ignore("Only use this locally for profiling")
@RunWith(Parameterized::class)
@ -83,12 +76,13 @@ class RPCPerformanceTests : AbstractRPCTest() {
val averageIndividualMs: Double,
val Mbps: Double
)
@Test
fun `measure Megabytes per second for simple RPCs`() {
warmup()
val inputOutputSizes = listOf(1024, 4096, 100 * 1024)
val overallTraffic = 512 * 1024 * 1024L
measure(inputOutputSizes, (1..5)) { inputOutputSize, N ->
measure(inputOutputSizes, (1..5)) { inputOutputSize, _ ->
rpcDriver {
val proxy = testProxy(
RPCClientConfiguration.default.copy(
@ -105,10 +99,9 @@ class RPCPerformanceTests : AbstractRPCTest() {
val numberOfRequests = overallTraffic / (2 * inputOutputSize)
val timings = Collections.synchronizedList(ArrayList<Long>())
val executor = Executors.newFixedThreadPool(8)
val totalElapsed = Stopwatch.createStarted().apply {
startInjectorWithBoundedQueue(
executor = executor,
startTightLoopInjector(
parallelism = 8,
numberOfInjections = numberOfRequests.toInt(),
queueBound = 100
) {
@ -118,7 +111,6 @@ class RPCPerformanceTests : AbstractRPCTest() {
timings.add(elapsed)
}
}.stop().elapsed(TimeUnit.MICROSECONDS)
executor.shutdownNow()
SimpleRPCResult(
requestPerSecond = 1000000.0 * numberOfRequests.toDouble() / totalElapsed.toDouble(),
averageIndividualMs = timings.average() / 1000.0,
@ -134,7 +126,7 @@ class RPCPerformanceTests : AbstractRPCTest() {
@Test
fun `consumption rate`() {
rpcDriver {
val metricRegistry = startReporter()
val metricRegistry = startReporter(shutdownManager)
val proxy = testProxy(
RPCClientConfiguration.default.copy(
reapInterval = 1.seconds,
@ -147,14 +139,13 @@ class RPCPerformanceTests : AbstractRPCTest() {
producerPoolBound = 8
)
)
measurePerformancePublishMetrics(
startPublishingFixedRateInjector(
metricRegistry = metricRegistry,
parallelism = 8,
overallDuration = 5.minutes,
injectionRate = 20000L / TimeUnit.SECONDS,
queueSizeMetricName = "$mode.QueueSize",
workDurationMetricName = "$mode.WorkDuration",
shutdownManager = this.shutdownManager,
work = {
proxy.ops.simpleReply(ByteArray(4096), 4096)
}
@ -176,19 +167,17 @@ class RPCPerformanceTests : AbstractRPCTest() {
consumerPoolSize = 1
)
)
val executor = Executors.newFixedThreadPool(clientParallelism)
val numberOfMessages = 1000
val bigSize = 10_000_000
val elapsed = Stopwatch.createStarted().apply {
startInjectorWithBoundedQueue(
executor = executor,
startTightLoopInjector(
parallelism = clientParallelism,
numberOfInjections = numberOfMessages,
queueBound = 4
) {
proxy.ops.simpleReply(ByteArray(bigSize), 0)
}
}.stop().elapsed(TimeUnit.MICROSECONDS)
executor.shutdownNow()
BigMessagesResult(
Mbps = bigSize.toDouble() * numberOfMessages.toDouble() / elapsed * (1000000.0 / (1024.0 * 1024.0))
)
@ -196,120 +185,3 @@ class RPCPerformanceTests : AbstractRPCTest() {
}.forEach(::println)
}
}
fun measurePerformancePublishMetrics(
metricRegistry: MetricRegistry,
parallelism: Int,
overallDuration: Duration,
injectionRate: Rate,
queueSizeMetricName: String,
workDurationMetricName: String,
shutdownManager: ShutdownManager,
work: () -> Unit
) {
val workSemaphore = Semaphore(0)
metricRegistry.register(queueSizeMetricName, Gauge { workSemaphore.availablePermits() })
val workDurationTimer = metricRegistry.timer(workDurationMetricName)
val executor = Executors.newSingleThreadScheduledExecutor()
val workExecutor = Executors.newFixedThreadPool(parallelism)
val timings = Collections.synchronizedList(ArrayList<Long>())
for (i in 1 .. parallelism) {
workExecutor.submit {
try {
while (true) {
workSemaphore.acquire()
workDurationTimer.time {
timings.add(
Stopwatch.createStarted().apply {
work()
}.stop().elapsed(TimeUnit.MICROSECONDS)
)
}
}
} catch (throwable: Throwable) {
throwable.printStackTrace()
}
}
}
val injector = executor.scheduleAtFixedRate(
{
workSemaphore.release((injectionRate * TimeUnit.SECONDS).toInt())
},
0,
1,
TimeUnit.SECONDS
)
shutdownManager.registerShutdown {
injector.cancel(true)
workExecutor.shutdownNow()
executor.shutdownNow()
workExecutor.awaitTermination(1, TimeUnit.SECONDS)
executor.awaitTermination(1, TimeUnit.SECONDS)
}
Thread.sleep(overallDuration.toMillis())
}
fun startInjectorWithBoundedQueue(
executor: ExecutorService,
numberOfInjections: Int,
queueBound: Int,
work: () -> Unit
) {
val remainingLatch = CountDownLatch(numberOfInjections)
val queuedCount = AtomicInteger(0)
val lock = ReentrantLock()
val canQueueAgain = lock.newCondition()
val injectorShutdown = AtomicBoolean(false)
val injector = thread(name = "injector") {
while (true) {
if (injectorShutdown.get()) break
executor.submit {
work()
if (queuedCount.decrementAndGet() < queueBound / 2) {
lock.withLock {
canQueueAgain.signal()
}
}
remainingLatch.countDown()
}
if (queuedCount.incrementAndGet() > queueBound) {
lock.withLock {
canQueueAgain.await()
}
}
}
}
remainingLatch.await()
injectorShutdown.set(true)
injector.join()
}
fun RPCDriverExposedDSLInterface.startReporter(): MetricRegistry {
val metricRegistry = MetricRegistry()
val jmxReporter = thread {
JmxReporter.
forRegistry(metricRegistry).
inDomain("net.corda").
createsObjectNamesWith { _, domain, name ->
// Make the JMX hierarchy a bit better organised.
val category = name.substringBefore('.')
val subName = name.substringAfter('.', "")
if (subName == "")
ObjectName("$domain:name=$category")
else
ObjectName("$domain:type=$category,name=$subName")
}.
build().
start()
}
val consoleReporter = thread {
ConsoleReporter.forRegistry(metricRegistry).build().start(1, TimeUnit.SECONDS)
}
shutdownManager.registerShutdown {
jmxReporter.interrupt()
consoleReporter.interrupt()
jmxReporter.join()
consoleReporter.join()
}
return metricRegistry
}

View File

@ -1,5 +1,4 @@
myLegalName : "CN=Bank A,O=Bank A,L=London,C=UK"
nearestCity : "London"
myLegalName : "CN=Bank A,O=Bank A,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10002"
@ -8,6 +7,6 @@ webAddress : "localhost:10004"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB"
}
useHTTPS : false

View File

@ -1,5 +1,4 @@
myLegalName : "CN=Bank B,O=Bank A,L=London,C=UK"
nearestCity : "London"
myLegalName : "CN=Bank B,O=Bank A,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10005"
@ -8,6 +7,6 @@ webAddress : "localhost:10007"
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
networkMapService : {
address : "localhost:10000"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=UK"
legalName : "CN=Network Map Service,O=R3,OU=corda,L=London,C=GB"
}
useHTTPS : false

View File

@ -1,5 +1,4 @@
myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=UK"
nearestCity : "London"
myLegalName : "CN=Notary Service,O=R3,OU=corda,L=London,C=GB"
keyStorePassword : "cordacadevpass"
trustStorePassword : "trustpass"
p2pAddress : "localhost:10000"

View File

@ -1,5 +1,5 @@
gradlePluginsVersion=0.12.2
kotlinVersion=1.1.2
gradlePluginsVersion=0.12.4
kotlinVersion=1.1.1
guavaVersion=21.0
bouncycastleVersion=1.56
bouncycastleVersion=1.57
typesafeConfigVersion=1.3.1

View File

@ -55,15 +55,6 @@ public class CordformNode {
config = config.withValue("myLegalName", ConfigValueFactory.fromAnyRef(name));
}
/**
* Set the nearest city to the node.
*
* @param nearestCity The name of the nearest city to the node.
*/
public void nearestCity(String nearestCity) {
config = config.withValue("nearestCity", ConfigValueFactory.fromAnyRef(nearestCity));
}
/**
* Set the Artemis P2P port for this node.
*

View File

@ -76,7 +76,7 @@ dependencies {
compile "io.requery:requery-kotlin:$requery_version"
// For AMQP serialisation.
compile "org.apache.qpid:proton-j:0.18.0"
compile "org.apache.qpid:proton-j:0.19.0"
}
configurations {
@ -91,3 +91,11 @@ task testJar(type: Jar) {
artifacts {
testArtifacts testJar
}
jar {
baseName 'corda-core'
}
publish {
name = jar.baseName
}

View File

@ -0,0 +1,30 @@
package net.corda.core
import java.util.*
import java.util.Spliterator.*
import java.util.stream.IntStream
import java.util.stream.Stream
import java.util.stream.StreamSupport
import kotlin.streams.asSequence
private fun IntProgression.spliteratorOfInt(): Spliterator.OfInt {
val kotlinIterator = iterator()
val javaIterator = object : PrimitiveIterator.OfInt {
override fun nextInt() = kotlinIterator.nextInt()
override fun hasNext() = kotlinIterator.hasNext()
override fun remove() = throw UnsupportedOperationException("remove")
}
val spliterator = Spliterators.spliterator(
javaIterator,
(1 + (last - first) / step).toLong(),
SUBSIZED or IMMUTABLE or NONNULL or SIZED or ORDERED or SORTED or DISTINCT
)
return if (step > 0) spliterator else object : Spliterator.OfInt by spliterator {
override fun getComparator() = Comparator.reverseOrder<Int>()
}
}
fun IntProgression.stream(): IntStream = StreamSupport.intStream(spliteratorOfInt(), false)
@Suppress("UNCHECKED_CAST") // When toArray has filled in the array, the component type is no longer T? but T (that may itself be nullable).
inline fun <reified T> Stream<out T>.toTypedArray() = toArray { size -> arrayOfNulls<T>(size) } as Array<T>

View File

@ -24,7 +24,6 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
import java.time.temporal.Temporal
import java.util.HashMap
import java.util.concurrent.*
import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer
@ -33,8 +32,8 @@ import java.util.zip.Deflater
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.collections.LinkedHashMap
import kotlin.concurrent.withLock
import kotlin.reflect.KClass
import kotlin.reflect.KProperty
val Int.days: Duration get() = Duration.ofDays(this.toLong())
@ -106,20 +105,11 @@ fun <T> ListenableFuture<T>.failure(executor: Executor, body: (Throwable) -> Uni
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) }
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
infix fun <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) = mapToArray(transform, iterator(), size)
inline fun <reified R> IntProgression.mapToArray(transform: (Int) -> R) = mapToArray(transform, iterator(), 1 + (last - first) / step)
inline fun <T, reified R> mapToArray(transform: (T) -> R, iterator: Iterator<T>, size: Int) = run {
var expected = 0
Array(size) {
expected++ == it || throw UnsupportedOperationException("Array constructor is non-sequential!")
transform(iterator.next())
}
}
/** Executes the given block and sets the future to either the result, or any exception that was thrown. */
inline fun <T> SettableFuture<T>.catch(block: () -> T) {
try {
@ -141,12 +131,18 @@ 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) = resolve(other)
operator fun String.div(other: String) = Paths.get(this) / other
operator fun Path.div(other: String): Path = resolve(other)
operator fun String.div(other: String): Path = Paths.get(this) / other
fun Path.createDirectory(vararg attrs: FileAttribute<*>): Path = Files.createDirectory(this, *attrs)
fun Path.createDirectories(vararg attrs: FileAttribute<*>): Path = Files.createDirectories(this, *attrs)
fun Path.exists(vararg options: LinkOption): Boolean = Files.exists(this, *options)
fun Path.copyToDirectory(targetDir: Path, vararg options: CopyOption): Path {
require(targetDir.isDirectory()) { "$targetDir is not a directory" }
val targetFile = targetDir.resolve(fileName)
Files.copy(this, targetFile, *options)
return targetFile
}
fun Path.moveTo(target: Path, vararg options: CopyOption): Path = Files.move(this, target, *options)
fun Path.isRegularFile(vararg options: LinkOption): Boolean = Files.isRegularFile(this, *options)
fun Path.isDirectory(vararg options: LinkOption): Boolean = Files.isDirectory(this, *options)
@ -472,4 +468,25 @@ fun <T> Class<T>.checkNotUnorderedHashMap() {
if (HashMap::class.java.isAssignableFrom(this) && !LinkedHashMap::class.java.isAssignableFrom(this)) {
throw NotSerializableException("Map type $this is unstable under iteration. Suggested fix: use LinkedHashMap instead.")
}
}
}
fun Class<*>.requireExternal(msg: String = "Internal class")
= require(!name.startsWith("net.corda.node.") && !name.contains(".internal.")) { "$msg: $name" }
interface DeclaredField<T> {
companion object {
inline fun <reified T> Any?.declaredField(clazz: KClass<*>, name: String): DeclaredField<T> = declaredField(clazz.java, name)
inline fun <reified T> Any.declaredField(name: String): DeclaredField<T> = declaredField(javaClass, name)
inline fun <reified T> Any?.declaredField(clazz: Class<*>, name: String): DeclaredField<T> {
val javaField = clazz.getDeclaredField(name).apply { isAccessible = true }
val receiver = this
return object : DeclaredField<T> {
override var value
get() = javaField.get(receiver) as T
set(value) = javaField.set(receiver, value)
}
}
}
var value: T
}

View File

@ -1,20 +1,8 @@
package net.corda.core.contracts
import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonDeserializer
import com.fasterxml.jackson.databind.JsonSerializer
import com.fasterxml.jackson.databind.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.google.common.annotations.VisibleForTesting
import net.corda.core.serialization.CordaSerializable
import java.math.BigDecimal
import java.math.RoundingMode
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.util.*
/**
@ -452,394 +440,3 @@ class AmountTransfer<T : Any, P : Any>(val quantityDelta: Long,
}
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Interest rate fixes
//
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */
@CordaSerializable
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
/** Represents a textual expression of e.g. a formula */
@CordaSerializable
@JsonDeserialize(using = ExpressionDeserializer::class)
@JsonSerialize(using = ExpressionSerializer::class)
data class Expression(val expr: String)
object ExpressionSerializer : JsonSerializer<Expression>() {
override fun serialize(expr: Expression, generator: JsonGenerator, provider: SerializerProvider) {
generator.writeString(expr.expr)
}
}
object ExpressionDeserializer : JsonDeserializer<Expression>() {
override fun deserialize(parser: JsonParser, context: DeserializationContext): Expression {
return Expression(parser.text)
}
}
/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
@CordaSerializable
data class Tenor(val name: String) {
private val amount: Int
private val unit: TimeUnit
init {
if (name == "ON") {
// Overnight
amount = 1
unit = TimeUnit.Day
} else {
val regex = """(\d+)([DMYW])""".toRegex()
val match = regex.matchEntire(name)?.groupValues ?: throw IllegalArgumentException("Unrecognised tenor name: $name")
amount = match[1].toInt()
unit = TimeUnit.values().first { it.code == match[2] }
}
}
fun daysToMaturity(startDate: LocalDate, calendar: BusinessCalendar): Int {
val maturityDate = when (unit) {
TimeUnit.Day -> startDate.plusDays(amount.toLong())
TimeUnit.Week -> startDate.plusWeeks(amount.toLong())
TimeUnit.Month -> startDate.plusMonths(amount.toLong())
TimeUnit.Year -> startDate.plusYears(amount.toLong())
else -> throw IllegalStateException("Invalid tenor time unit: $unit")
}
// Move date to the closest business day when it falls on a weekend/holiday
val adjustedMaturityDate = calendar.applyRollConvention(maturityDate, DateRollConvention.ModifiedFollowing)
val daysToMaturity = calculateDaysBetween(startDate, adjustedMaturityDate, DayCountBasisYear.Y360, DayCountBasisDay.DActual)
return daysToMaturity
}
override fun toString(): String = name
@CordaSerializable
enum class TimeUnit(val code: String) {
Day("D"), Week("W"), Month("M"), Year("Y")
}
}
/**
* Simple enum for returning accurals adjusted or unadjusted.
* We don't actually do anything with this yet though, so it's ignored for now.
*/
@CordaSerializable
enum class AccrualAdjustment {
Adjusted, Unadjusted
}
/**
* This is utilised in the [DateRollConvention] class to determine which way we should initially step when
* finding a business day.
*/
@CordaSerializable
enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
/**
* This reflects what happens if a date on which a business event is supposed to happen actually falls upon a non-working day.
* Depending on the accounting requirement, we can move forward until we get to a business day, or backwards.
* There are some additional rules which are explained in the individual cases below.
*/
@CordaSerializable
enum class DateRollConvention(val direction: () -> DateRollDirection, val isModified: Boolean) {
// direction() cannot be a val due to the throw in the Actual instance
/** Don't roll the date, use the one supplied. */
Actual({ throw UnsupportedOperationException("Direction is not relevant for convention Actual") }, false),
/** Following is the next business date from this one. */
Following({ DateRollDirection.FORWARD }, false),
/**
* "Modified following" is the next business date, unless it's in the next month, in which case use the preceeding
* business date.
*/
ModifiedFollowing({ DateRollDirection.FORWARD }, true),
/** Previous is the previous business date from this one. */
Previous({ DateRollDirection.BACKWARD }, false),
/**
* Modified previous is the previous business date, unless it's in the previous month, in which case use the next
* business date.
*/
ModifiedPrevious({ DateRollDirection.BACKWARD }, true);
}
/**
* This forms the day part of the "Day Count Basis" used for interest calculation.
* Note that the first character cannot be a number (enum naming constraints), so we drop that
* in the toString lest some people get confused.
*/
@CordaSerializable
enum class DayCountBasisDay {
// We have to prefix 30 etc with a letter due to enum naming constraints.
D30,
D30N, D30P, D30E, D30G, DActual, DActualJ, D30Z, D30F, DBus_SaoPaulo;
override fun toString(): String {
return super.toString().drop(1)
}
}
/** This forms the year part of the "Day Count Basis" used for interest calculation. */
@CordaSerializable
enum class DayCountBasisYear {
// Ditto above comment for years.
Y360,
Y365F, Y365L, Y365Q, Y366, YActual, YActualA, Y365B, Y365, YISMA, YICMA, Y252;
override fun toString(): String {
return super.toString().drop(1)
}
}
/** Whether the payment should be made before the due date, or after it. */
@CordaSerializable
enum class PaymentRule {
InAdvance, InArrears,
}
/**
* Frequency at which an event occurs - the enumerator also casts to an integer specifying the number of times per year
* that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc).
*/
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
@CordaSerializable
enum class Frequency(val annualCompoundCount: Int, val offset: LocalDate.(Long) -> LocalDate) {
Annual(1, { plusYears(1 * it) }),
SemiAnnual(2, { plusMonths(6 * it) }),
Quarterly(4, { plusMonths(3 * it) }),
Monthly(12, { plusMonths(1 * it) }),
Weekly(52, { plusWeeks(1 * it) }),
BiWeekly(26, { plusWeeks(2 * it) }),
Daily(365, { plusDays(1 * it) });
}
@Suppress("unused") // This utility may be useful in future. TODO: Review before API stability guarantees in place.
fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = accordingToCalendar.isWorkingDay(this)
// TODO: Make Calendar data come from an oracle
/**
* A business calendar performs date calculations that take into account national holidays and weekends. This is a
* typical feature of financial contracts, in which a business may not want a payment event to fall on a day when
* no staff are around to handle problems.
*/
@CordaSerializable
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
@CordaSerializable
class UnknownCalendar(name: String) : Exception("$name not found")
companion object {
val calendars = listOf("London", "NewYork")
val TEST_CALENDAR_DATA = calendars.map {
it to BusinessCalendar::class.java.getResourceAsStream("${it}HolidayCalendar.txt").bufferedReader().readText()
}.toMap()
/** Parses a date of the form YYYY-MM-DD, like 2016-01-10 for 10th Jan. */
fun parseDateFromString(it: String): LocalDate = LocalDate.parse(it, DateTimeFormatter.ISO_LOCAL_DATE)
/** Returns a business calendar that combines all the named holiday calendars into one list of holiday dates. */
fun getInstance(vararg calname: String) = BusinessCalendar(
calname.flatMap { (TEST_CALENDAR_DATA[it] ?: throw UnknownCalendar(it)).split(",") }.
toSet().
map { parseDateFromString(it) }.
toList().sorted()
)
/** Calculates an event schedule that moves events around to ensure they fall on working days. */
fun createGenericSchedule(startDate: LocalDate,
period: Frequency,
calendar: BusinessCalendar = getInstance(),
dateRollConvention: DateRollConvention = DateRollConvention.Following,
noOfAdditionalPeriods: Int = Integer.MAX_VALUE,
endDate: LocalDate? = null,
periodOffset: Int? = null): List<LocalDate> {
val ret = ArrayList<LocalDate>()
var ctr = 0
var currentDate = startDate
while (true) {
currentDate = getOffsetDate(currentDate, period)
if (periodOffset == null || periodOffset <= ctr)
ret.add(calendar.applyRollConvention(currentDate, dateRollConvention))
ctr += 1
// TODO: Fix addl period logic
if ((ctr > noOfAdditionalPeriods) || (currentDate >= endDate ?: currentDate))
break
}
return ret
}
/** Calculates the date from @startDate moving forward @steps of time size @period. Does not apply calendar
* logic / roll conventions.
*/
fun getOffsetDate(startDate: LocalDate, period: Frequency, steps: Int = 1): LocalDate {
if (steps == 0) return startDate
return period.offset(startDate, steps.toLong())
}
}
override fun equals(other: Any?): Boolean = if (other is BusinessCalendar) {
/** Note this comparison is OK as we ensure they are sorted in getInstance() */
this.holidayDates == other.holidayDates
} else {
false
}
override fun hashCode(): Int {
return this.holidayDates.hashCode()
}
open fun isWorkingDay(date: LocalDate): Boolean =
when {
date.dayOfWeek == DayOfWeek.SATURDAY -> false
date.dayOfWeek == DayOfWeek.SUNDAY -> false
holidayDates.contains(date) -> false
else -> true
}
open fun applyRollConvention(testDate: LocalDate, dateRollConvention: DateRollConvention): LocalDate {
if (dateRollConvention == DateRollConvention.Actual) return testDate
var direction = dateRollConvention.direction().value
var trialDate = testDate
while (!isWorkingDay(trialDate)) {
trialDate = trialDate.plusDays(direction)
}
// We've moved to the next working day in the right direction, but if we're using the "modified" date roll
// convention and we've crossed into another month, reverse the direction instead to stay within the month.
// Probably better explained here: http://www.investopedia.com/terms/m/modifiedfollowing.asp
if (dateRollConvention.isModified && testDate.month != trialDate.month) {
direction = -direction
trialDate = testDate
while (!isWorkingDay(trialDate)) {
trialDate = trialDate.plusDays(direction)
}
}
return trialDate
}
/**
* Returns a date which is the inbound date plus/minus a given number of business days.
* TODO: Make more efficient if necessary
*/
fun moveBusinessDays(date: LocalDate, direction: DateRollDirection, i: Int): LocalDate {
require(i >= 0)
if (i == 0) return date
var retDate = date
var ctr = 0
while (ctr < i) {
retDate = retDate.plusDays(direction.value)
if (isWorkingDay(retDate)) ctr++
}
return retDate
}
}
fun calculateDaysBetween(startDate: LocalDate,
endDate: LocalDate,
dcbYear: DayCountBasisYear,
dcbDay: DayCountBasisDay): Int {
// Right now we are only considering Actual/360 and 30/360 .. We'll do the rest later.
// TODO: The rest.
return when {
dcbDay == DayCountBasisDay.DActual -> (endDate.toEpochDay() - startDate.toEpochDay()).toInt()
dcbDay == DayCountBasisDay.D30 && dcbYear == DayCountBasisYear.Y360 -> ((endDate.year - startDate.year) * 360.0 + (endDate.monthValue - startDate.monthValue) * 30.0 + endDate.dayOfMonth - startDate.dayOfMonth).toInt()
else -> TODO("Can't calculate days using convention $dcbDay / $dcbYear")
}
}
/**
* Enum for the types of netting that can be applied to state objects. Exact behaviour
* for each type of netting is left to the contract to determine.
*/
@CordaSerializable
enum class NetType {
/**
* Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific),
* and allows their counterparty to net obligations without requiring approval from all parties. For example, if
* Bank A owes Bank B £1m, and Bank B owes Bank A £1m, in the case of Bank B defaulting this would enable Bank A
* to net out the two obligations to zero, rather than being legally obliged to pay £1m without any realistic
* expectation of the debt to them being paid. Realistically this is limited to bilateral netting, to simplify
* determining which party must sign the netting transaction.
*/
CLOSE_OUT,
/**
* "Payment" is used to refer to conventional netting, where all parties must confirm the netting transaction. This
* can be a multilateral netting transaction, and may be created by a central clearing service.
*/
PAYMENT
}
/**
* Class representing a commodity, as an equivalent to the [Currency] class. This exists purely to enable the
* [CommodityContract] contract, and is likely to change in future.
*
* @param commodityCode a unique code for the commodity. No specific registry for these is currently defined, although
* this is likely to change in future.
* @param displayName human readable name for the commodity.
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
* this commodity.
*/
@CordaSerializable
data class Commodity(val commodityCode: String,
val displayName: String,
val defaultFractionDigits: Int = 0) : TokenizableAssetInfo {
override val displayTokenSize: BigDecimal
get() = BigDecimal.ONE.scaleByPowerOfTen(-defaultFractionDigits)
companion object {
private val registry = mapOf(
// Simple example commodity, as in http://www.investopedia.com/university/commodities/commodities14.asp
Pair("FCOJ", Commodity("FCOJ", "Frozen concentrated orange juice"))
)
fun getInstance(commodityCode: String): Commodity?
= registry[commodityCode]
}
}
/**
* This class provides a truly unique identifier of a trade, state, or other business object, bound to any existing
* external ID. Equality and comparison are based on the unique ID only; if two states somehow have the same UUID but
* different external IDs, it would indicate a problem with handling of IDs.
*
* @param externalId Any existing weak identifier such as trade reference ID.
* This should be set here the first time a [UniqueIdentifier] is created as part of state issuance,
* or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong ID.
* @param id Should never be set by user code and left as default initialised.
* So that the first time a state is issued this should be given a new UUID.
* Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified.
*/
@CordaSerializable
data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable<UniqueIdentifier> {
override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString()
companion object {
/** Helper function for unit tests where the UUID needs to be manually initialised for consistency. */
@VisibleForTesting
fun fromString(name: String): UniqueIdentifier = UniqueIdentifier(null, UUID.fromString(name))
}
override fun compareTo(other: UniqueIdentifier): Int = id.compareTo(other.id)
override fun equals(other: Any?): Boolean {
return if (other is UniqueIdentifier)
id == other.id
else
false
}
override fun hashCode(): Int = id.hashCode()
}

View File

@ -3,8 +3,8 @@
package net.corda.core.contracts
import net.corda.core.identity.Party
import java.security.PublicKey
import java.math.BigDecimal
import java.security.PublicKey
import java.util.*
/**
@ -22,15 +22,12 @@ import java.util.*
fun currency(code: String) = Currency.getInstance(code)!!
fun commodity(code: String) = Commodity.getInstance(code)!!
@JvmField val USD = currency("USD")
@JvmField val GBP = currency("GBP")
@JvmField val EUR = currency("EUR")
@JvmField val CHF = currency("CHF")
@JvmField val JPY = currency("JPY")
@JvmField val RUB = currency("RUB")
@JvmField val FCOJ = commodity("FCOJ") // Frozen concentrated orange juice, yum!
fun <T : Any> AMOUNT(amount: Int, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount.toLong()), token)
fun <T : Any> AMOUNT(amount: Double, token: T): Amount<T> = Amount.fromDecimal(BigDecimal.valueOf(amount), token)
@ -38,19 +35,15 @@ fun DOLLARS(amount: Int): Amount<Currency> = AMOUNT(amount, USD)
fun DOLLARS(amount: Double): Amount<Currency> = AMOUNT(amount, USD)
fun POUNDS(amount: Int): Amount<Currency> = AMOUNT(amount, GBP)
fun SWISS_FRANCS(amount: Int): Amount<Currency> = AMOUNT(amount, CHF)
fun FCOJ(amount: Int): Amount<Commodity> = AMOUNT(amount, FCOJ)
val Int.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Double.DOLLARS: Amount<Currency> get() = DOLLARS(this)
val Int.POUNDS: Amount<Currency> get() = POUNDS(this)
val Int.SWISS_FRANCS: Amount<Currency> get() = SWISS_FRANCS(this)
val Int.FCOJ: Amount<Commodity> get() = FCOJ(this)
infix fun Currency.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Commodity.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Amount<Currency>.`issued by`(deposit: PartyAndReference) = issuedBy(deposit)
infix fun Currency.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Commodity.issuedBy(deposit: PartyAndReference) = Issued(deposit, this)
infix fun Amount<Currency>.issuedBy(deposit: PartyAndReference) = Amount(quantity, displayTokenSize, token.issuedBy(deposit))
//// Requirements /////////////////////////////////////////////////////////////////////////////////////////////////////

View File

@ -5,11 +5,8 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowLogicRef
import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.*
import net.corda.core.transactions.TransactionBuilder
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
@ -24,37 +21,7 @@ interface NamedByHash {
val id: SecureHash
}
/**
* Interface for state objects that support being netted with other state objects.
*/
interface BilateralNettableState<N : BilateralNettableState<N>> {
/**
* Returns an object used to determine if two states can be subject to close-out netting. If two states return
* equal objects, they can be close out netted together.
*/
val bilateralNetState: Any
/**
* Perform bilateral netting of this state with another state. The two states must be compatible (as in
* bilateralNetState objects are equal).
*/
fun net(other: N): N
}
/**
* Interface for state objects that support being netted with other state objects.
*/
interface MultilateralNettableState<out T : Any> {
/**
* Returns an object used to determine if two states can be subject to close-out netting. If two states return
* equal objects, they can be close out netted together.
*/
val multilateralNetState: T
}
interface NettableState<N : BilateralNettableState<N>, out T : Any> : BilateralNettableState<N>,
MultilateralNettableState<T>
// DOCSTART 1
/**
* A contract state (or just "state") contains opaque data used by a contract program. It can be thought of as a disk
* file that the program can use to persist data across transactions. States are immutable: once created they are never
@ -117,7 +84,9 @@ interface ContractState {
*/
val participants: List<AbstractParty>
}
// DOCEND 1
// DOCSTART 4
/**
* A wrapper for [ContractState] containing additional platform-level state information.
* This is the definitive state that is stored on the ledger and used in transaction outputs.
@ -146,6 +115,7 @@ data class TransactionState<out T : ContractState> @JvmOverloads constructor(
* otherwise the transaction is not valid.
*/
val encumbrance: Int? = null)
// DOCEND 4
/** Wraps the [ContractState] in a [TransactionState] object */
infix fun <T : ContractState> T.`with notary`(newNotary: Party) = withNotary(newNotary)
@ -170,6 +140,7 @@ data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
*/
fun <T : Any> Amount<Issued<T>>.withoutIssuer(): Amount<T> = Amount(quantity, token.product)
// DOCSTART 3
/**
* A contract state that can have a single owner.
*/
@ -180,6 +151,7 @@ interface OwnableState : ContractState {
/** Copies the underlying data structure, replacing the owner field with this new value and leaving the rest alone */
fun withNewOwner(newOwner: AbstractParty): Pair<CommandData, OwnableState>
}
// DOCEND 3
/** Something which is scheduled to happen at a point in time */
interface Scheduled {
@ -208,6 +180,7 @@ data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instan
*/
data class ScheduledActivity(val logicRef: FlowLogicRef, override val scheduledAt: Instant) : Scheduled
// DOCSTART 2
/**
* A state that evolves by superseding itself, all of which share the common "linearId".
*
@ -246,6 +219,7 @@ interface LinearState : ContractState {
}
}
}
// DOCEND 2
interface SchedulableState : ContractState {
/**
@ -260,64 +234,6 @@ interface SchedulableState : ContractState {
fun nextScheduledActivity(thisStateRef: StateRef, flowLogicRefFactory: FlowLogicRefFactory): ScheduledActivity?
}
/**
* Interface representing an agreement that exposes various attributes that are common. Implementing it simplifies
* implementation of general flows that manipulate many agreement types.
*/
interface DealState : LinearState {
/** Human readable well known reference (e.g. trade reference) */
val ref: String
/**
* Exposes the Parties involved in a generic way.
*
* Appears to duplicate [participants] a property of [ContractState]. However [participants] only holds public keys.
* Currently we need to hard code Party objects into [ContractState]s. [Party] objects are a wrapper for public
* keys which also contain some identity information about the public key owner. You can keep track of individual
* parties by adding a property for each one to the state, or you can append parties to the [parties] list if you
* are implementing [DealState]. We need to do this as identity management in Corda is currently incomplete,
* therefore the only way to record identity information is in the [ContractState]s themselves. When identity
* management is completed, parties to a transaction will only record public keys in the [DealState] and through a
* separate process exchange certificates to ascertain identities. Thus decoupling identities from
* [ContractState]s.
* */
val parties: List<AbstractParty>
/**
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general
* deal/agreement flow to generate the necessary transaction for potential implementations.
*
* TODO: Currently this is the "inception" transaction but in future an offer of some description might be an input state ref
*
* TODO: This should more likely be a method on the Contract (on a common interface) and the changes to reference a
* Contract instance from a ContractState are imminent, at which point we can move this out of here.
*/
fun generateAgreement(notary: Party): TransactionBuilder
}
/**
* Interface adding fixing specific methods.
*/
interface FixableDealState : DealState {
/**
* When is the next fixing and what is the fixing for?
*/
fun nextFixingOf(): FixOf?
/**
* What oracle service to use for the fixing
*/
val oracleType: ServiceType
/**
* Generate a fixing command for this deal and fix.
*
* TODO: This would also likely move to methods on the Contract once the changes to reference
* the Contract from the ContractState are in.
*/
fun generateFix(ptx: TransactionBuilder, oldState: StateAndRef<*>, fix: Fix)
}
/** Returns the SHA-256 hash of the serialised contents of this state (not cached!) */
fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes)
@ -326,13 +242,17 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes)
* transaction defined the state and where in that transaction it was.
*/
@CordaSerializable
// DOCSTART 8
data class StateRef(val txhash: SecureHash, val index: Int) {
override fun toString() = "$txhash($index)"
}
// DOCEND 8
/** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */
@CordaSerializable
// DOCSTART 7
data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, val ref: StateRef)
// DOCEND 7
/** Filters a list of [StateAndRef] objects according to the type of the states */
inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filterStatesOfType(): List<StateAndRef<T>> {
@ -360,7 +280,9 @@ abstract class TypeOnlyCommandData : CommandData {
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
@CordaSerializable
// DOCSTART 9
data class Command(val value: CommandData, val signers: List<PublicKey>) {
// DOCEND 9
init {
require(signers.isNotEmpty())
}
@ -387,15 +309,10 @@ interface MoveCommand : CommandData {
val contractHash: SecureHash?
}
/** A common netting command for contracts whose states can be netted. */
interface NetCommand : CommandData {
/** The type of netting to apply, see [NetType] for options. */
val type: NetType
}
/** Indicates that this transaction replaces the inputs contract state to another contract state */
data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<*, *>>) : CommandData
// DOCSTART 6
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
@CordaSerializable
data class AuthenticatedObject<out T : Any>(
@ -404,35 +321,71 @@ data class AuthenticatedObject<out T : Any>(
val signingParties: List<Party>,
val value: T
)
// DOCEND 6
/**
* A time-window is required for validation/notarization purposes.
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
* between (after, before).
* between (fromTime, untilTime).
* Usually, a time-window is required to have both sides set (fromTime, untilTime).
* However, some apps may require that a time-window has a start [Instant] (fromTime), but no end [Instant] (untilTime) and vice versa.
* TODO: Consider refactoring using TimeWindow abstraction like TimeWindow.From, TimeWindow.Until, TimeWindow.Between.
*/
@CordaSerializable
data class Timestamp(
/** The time at which this transaction is said to have occurred is after this moment */
val after: Instant?,
/** The time at which this transaction is said to have occurred is before this moment */
val before: Instant?
class TimeWindow private constructor(
/** The time at which this transaction is said to have occurred is after this moment. */
val fromTime: Instant?,
/** The time at which this transaction is said to have occurred is before this moment. */
val untilTime: Instant?
) {
init {
if (after == null && before == null)
throw IllegalArgumentException("At least one of before/after must be specified")
if (after != null && before != null)
check(after <= before)
companion object {
/** Use when the left-side [fromTime] of a [TimeWindow] is only required and we don't need an end instant (untilTime). */
@JvmStatic
fun fromOnly(fromTime: Instant) = TimeWindow(fromTime, null)
/** Use when the right-side [untilTime] of a [TimeWindow] is only required and we don't need a start instant (fromTime). */
@JvmStatic
fun untilOnly(untilTime: Instant) = TimeWindow(null, untilTime)
/** Use when both sides of a [TimeWindow] must be set ([fromTime], [untilTime]). */
@JvmStatic
fun between(fromTime: Instant, untilTime: Instant): TimeWindow {
require(fromTime < untilTime) { "fromTime should be earlier than untilTime" }
return TimeWindow(fromTime, untilTime)
}
/** Use when we have a start time and a period of validity. */
@JvmStatic
fun fromStartAndDuration(fromTime: Instant, duration: Duration): TimeWindow = between(fromTime, fromTime + duration)
/**
* When we need to create a [TimeWindow] based on a specific time [Instant] and some tolerance in both sides of this instant.
* The result will be the following time-window: ([time] - [tolerance], [time] + [tolerance]).
*/
@JvmStatic
fun withTolerance(time: Instant, tolerance: Duration) = between(time - tolerance, time + tolerance)
}
constructor(time: Instant, tolerance: Duration) : this(time - tolerance, time + tolerance)
/** The midpoint is calculated as fromTime + (untilTime - fromTime)/2. Note that it can only be computed if both sides are set. */
val midpoint: Instant get() = fromTime!! + Duration.between(fromTime, untilTime!!).dividedBy(2)
val midpoint: Instant get() = after!! + Duration.between(after, before!!).dividedBy(2)
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is TimeWindow) return false
return (fromTime == other.fromTime && untilTime == other.untilTime)
}
override fun hashCode() = 31 * (fromTime?.hashCode() ?: 0) + (untilTime?.hashCode() ?: 0)
override fun toString() = "TimeWindow(fromTime=$fromTime, untilTime=$untilTime)"
}
// DOCSTART 5
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the
* transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted
* timestamp attached to the transaction itself i.e. it is NOT necessarily the current time.
* time-window attached to the transaction itself i.e. it is NOT necessarily the current time.
*
* TODO: Contract serialization is likely to change, so the annotation is likely temporary.
*/
@ -453,6 +406,7 @@ interface Contract {
*/
val legalContractReference: SecureHash
}
// DOCEND 5
/**
* Interface which can upgrade state objects issued by a contract to a new state object issued by a different contract.
@ -510,7 +464,7 @@ abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
val storage = serviceHub.storageService.attachments
return {
val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id))
if (a is AbstractAttachment) a.attachmentData else a.open().use { it.readBytes() }
(a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() }
}
}
}

View File

@ -19,7 +19,7 @@ sealed class TransactionType {
*/
@Throws(TransactionVerificationException::class)
fun verify(tx: LedgerTransaction) {
require(tx.notary != null || tx.timestamp == null) { "Transactions with timestamps must be notarised." }
require(tx.notary != null || tx.timeWindow == null) { "Transactions with time-windows must be notarised" }
val duplicates = detectDuplicateInputs(tx)
if (duplicates.isNotEmpty()) throw TransactionVerificationException.DuplicateInputStates(tx.id, duplicates)
val missing = verifySigners(tx)

View File

@ -1,8 +1,8 @@
package net.corda.core.contracts
import net.corda.core.identity.Party
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowException
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import java.security.PublicKey
import java.util.*
@ -13,13 +13,15 @@ import java.util.*
* A transaction to be passed as input to a contract verification function. Defines helper methods to
* simplify verification logic in contracts.
*/
// DOCSTART 1
data class TransactionForContract(val inputs: List<ContractState>,
val outputs: List<ContractState>,
val attachments: List<Attachment>,
val commands: List<AuthenticatedObject<CommandData>>,
val origHash: SecureHash,
val inputNotary: Party? = null,
val timestamp: Timestamp? = null) {
val timeWindow: TimeWindow? = null) {
// DOCEND 1
override fun hashCode() = origHash.hashCode()
override fun equals(other: Any?) = other is TransactionForContract && other.origHash == origHash
@ -37,6 +39,7 @@ data class TransactionForContract(val inputs: List<ContractState>,
* currency. To solve this, you would use groupStates with a type of Cash.State and a selector that returns the
* currency field: the resulting list can then be iterated over to perform the per-currency calculation.
*/
// DOCSTART 2
fun <T : ContractState, K : Any> groupStates(ofType: Class<T>, selector: (T) -> K): List<InOutGroup<T, K>> {
val inputs = inputs.filterIsInstance(ofType)
val outputs = outputs.filterIsInstance(ofType)
@ -47,6 +50,7 @@ data class TransactionForContract(val inputs: List<ContractState>,
@Suppress("DEPRECATION")
return groupStatesInternal(inGroups, outGroups)
}
// DOCEND 2
/** See the documentation for the reflection-based version of [groupStates] */
inline fun <reified T : ContractState, K : Any> groupStates(selector: (T) -> K): List<InOutGroup<T, K>> {
@ -83,7 +87,9 @@ data class TransactionForContract(val inputs: List<ContractState>,
* up on both sides of the transaction, but the values must be summed independently per currency. Grouping can
* be used to simplify this logic.
*/
// DOCSTART 3
data class InOutGroup<out T : ContractState, out K : Any>(val inputs: List<T>, val outputs: List<T>, val groupingKey: K)
// DOCEND 3
}
class TransactionResolutionException(val hash: SecureHash) : FlowException() {

View File

@ -0,0 +1,39 @@
package net.corda.core.contracts
import com.google.common.annotations.VisibleForTesting
import net.corda.core.serialization.CordaSerializable
import java.util.*
/**
* This class provides a truly unique identifier of a trade, state, or other business object, bound to any existing
* external ID. Equality and comparison are based on the unique ID only; if two states somehow have the same UUID but
* different external IDs, it would indicate a problem with handling of IDs.
*
* @param externalId Any existing weak identifier such as trade reference ID.
* This should be set here the first time a [UniqueIdentifier] is created as part of state issuance,
* or ledger on-boarding activity. This ensure that the human readable identity is paired with the strong ID.
* @param id Should never be set by user code and left as default initialised.
* So that the first time a state is issued this should be given a new UUID.
* Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified.
*/
@CordaSerializable
data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable<UniqueIdentifier> {
override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString()
companion object {
/** Helper function for unit tests where the UUID needs to be manually initialised for consistency. */
@VisibleForTesting
fun fromString(name: String): UniqueIdentifier = UniqueIdentifier(null, UUID.fromString(name))
}
override fun compareTo(other: UniqueIdentifier): Int = id.compareTo(other.id)
override fun equals(other: Any?): Boolean {
return if (other is UniqueIdentifier)
id == other.id
else
false
}
override fun hashCode(): Int = id.hashCode()
}

View File

@ -1,7 +1,9 @@
package net.corda.core.crypto
import net.corda.core.crypto.CompositeKey.NodeAndWeight
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.serialize
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.security.PublicKey
/**
@ -35,20 +37,24 @@ class CompositeKey private constructor (val threshold: Int,
* Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
*/
@CordaSerializable
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight> {
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight>, ASN1Object() {
override fun compareTo(other: NodeAndWeight): Int {
if (weight == other.weight) {
return node.hashCode().compareTo(other.node.hashCode())
}
else return weight.compareTo(other.weight)
}
override fun toASN1Primitive(): ASN1Primitive {
val vector = ASN1EncodableVector()
vector.add(DERBitString(node.encoded))
vector.add(ASN1Integer(weight.toLong()))
return DERSequence(vector)
}
}
companion object {
// TODO: Get the design standardised and from there define a recognised name
val ALGORITHM = "X-Corda-CompositeKey"
// TODO: We should be using a well defined format.
val FORMAT = "X-Corda-Kryo"
val ALGORITHM = CompositeSignature.ALGORITHM_IDENTIFIER.algorithm.toString()
}
/**
@ -57,8 +63,17 @@ class CompositeKey private constructor (val threshold: Int,
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
override fun getAlgorithm() = ALGORITHM
override fun getEncoded(): ByteArray = this.serialize().bytes
override fun getFormat() = FORMAT
override fun getEncoded(): ByteArray {
val keyVector = ASN1EncodableVector()
val childrenVector = ASN1EncodableVector()
children.forEach {
childrenVector.add(it.toASN1Primitive())
}
keyVector.add(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector))
return SubjectPublicKeyInfo(CompositeSignature.ALGORITHM_IDENTIFIER, DERSequence(keyVector)).encoded
}
override fun getFormat() = ASN1Encoding.DER
/**
* Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite

View File

@ -9,6 +9,7 @@ import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.bc.BCObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
@ -19,8 +20,9 @@ import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder
import org.bouncycastle.cert.bc.BcX509ExtensionUtils
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
@ -29,6 +31,14 @@ import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.jcajce.provider.util.AsymmetricKeyInfoConverter
import org.bouncycastle.jce.ECNamedCurveTable
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.jce.spec.ECParameterSpec
import org.bouncycastle.jce.spec.ECPrivateKeySpec
import org.bouncycastle.jce.spec.ECPublicKeySpec
import org.bouncycastle.math.ec.ECConstants
import org.bouncycastle.math.ec.FixedPointCombMultiplier
import org.bouncycastle.math.ec.WNafUtil
import org.bouncycastle.operator.ContentSigner
import org.bouncycastle.operator.jcajce.JcaContentVerifierProviderBuilder
import org.bouncycastle.pkcs.PKCS10CertificationRequest
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
@ -42,11 +52,12 @@ import java.math.BigInteger
import java.security.*
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.cert.X509Certificate
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
import java.util.*
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
/**
* This object controls and provides the available and supported signature schemes for Corda.
@ -242,8 +253,7 @@ object Crypto {
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePrivateKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PrivateKey {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
@ -298,8 +308,7 @@ object Crypto {
*/
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
fun decodePublicKey(signatureScheme: SignatureScheme, encodedKey: ByteArray): PublicKey {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
@ -345,8 +354,7 @@ object Crypto {
*/
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun doSign(signatureScheme: SignatureScheme, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
signature.initSign(privateKey)
@ -425,8 +433,7 @@ object Crypto {
*/
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
fun doVerify(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
val verificationResult = isValid(signatureScheme, publicKey, signatureData, clearData)
@ -488,8 +495,7 @@ object Crypto {
*/
@Throws(SignatureException::class, IllegalArgumentException::class)
fun isValid(signatureScheme: SignatureScheme, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
val signature = Signature.getInstance(signatureScheme.signatureName, providerMap[signatureScheme.providerName])
signature.initVerify(publicKey)
signature.update(clearData)
@ -516,8 +522,7 @@ object Crypto {
@Throws(IllegalArgumentException::class)
@JvmOverloads
fun generateKeyPair(signatureScheme: SignatureScheme = DEFAULT_SIGNATURE_SCHEME): KeyPair {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
val keyPairGenerator = KeyPairGenerator.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName])
if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
@ -526,6 +531,143 @@ object Crypto {
return keyPairGenerator.generateKeyPair()
}
/**
* Deterministically generate/derive a [KeyPair] using an existing private key and a seed as inputs.
* This operation is currently supported for ECDSA secp256r1 (NIST P-256), ECDSA secp256k1 and EdDSA ed25519.
*
* Similarly to BIP32, the implemented algorithm uses an HMAC function based on SHA512 and it is actually
* an implementation the HKDF rfc - Step 1: Extract function,
* @see <a href="https://tools.ietf.org/html/rfc5869">HKDF</a>
* which is practically a variation of the private-parent-key -> private-child-key hardened key generation of BIP32.
*
* Unlike BIP32, where both private and public keys are extended to prevent deterministically
* generated child keys from depending solely on the key itself, current method uses normal elliptic curve keys
* without a chain-code and the generated key relies solely on the security of the private key.
*
* Although without a chain-code we lose the aforementioned property of not depending solely on the key,
* it should be mentioned that the cryptographic strength of the HMAC depends upon the size of the secret key.
* @see <a href="https://en.wikipedia.org/wiki/Hash-based_message_authentication_code#Security">HMAC Security</a>
* Thus, as long as the master key is kept secret and has enough entropy (~256 bits for EC-schemes), the system
* is considered secure.
*
* It is also a fact that if HMAC is used as PRF and/or MAC but not as checksum function, the function is still
* secure even if the underlying hash function is not collision resistant (e.g. if we used MD5).
* In practice, for our DKG purposes (thus PRF), a collision would not necessarily reveal the master HMAC key,
* because multiple inputs can produce the same hash output.
*
* Also according to the HMAC-based Extract-and-Expand Key Derivation Function (HKDF) rfc5869:
* <p><ul>
* <li>a chain-code (aka the salt) is recommended, but not required.
* <li>the salt can be public, but a hidden one provides stronger security guarantee.
* <li>even a simple counter can work as a salt, but ideally it should be random.
* <li>salt values should not be chosen by an attacker.
* </ul></p>
*
* Regarding the last requirement, according to Krawczyk's HKDF scheme: <i>While there is no need to keep the salt secret,
* it is assumed that salt values are independent of the input keying material</i>.
* @see <a href="http://eprint.iacr.org/2010/264.pdf">Cryptographic Extraction and Key Derivation - The HKDF Scheme</a>.
*
* There are also protocols that require an authenticated nonce (e.g. when a DH derived key is used as a seed) and thus
* we need to make sure that nonces come from legitimate parties rather than selected by an attacker.
* Similarly, in DLT systems, proper handling is required if users should agree on a common value as a seed,
* e.g. a transaction's nonce or hash.
*
* Moreover if a unique key per transaction is prerequisite, an attacker should never force a party to reuse a
* previously used key, due to privacy and forward secrecy reasons.
*
* All in all, this algorithm can be used with a counter as seed, however it is suggested that the output does
* not solely depend on the key, i.e. a secret salt per user or a random nonce per transaction could serve this role.
* In case where a non-random seed policy is selected, such as the BIP32 counter logic, one needs to carefully keep state
* so that the same salt is used only once.
*
* @param signatureScheme the [SignatureScheme] of the private key input.
* @param privateKey the [PrivateKey] that will be used as key to the HMAC-ed DKG function.
* @param seed an extra seed that will be used as value to the underlying HMAC.
* @return a new deterministically generated [KeyPair].
* @throws IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/
fun deterministicKeyPair(signatureScheme: SignatureScheme, privateKey: PrivateKey, seed: ByteArray): KeyPair {
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
when (signatureScheme) {
ECDSA_SECP256R1_SHA256, ECDSA_SECP256K1_SHA256 -> return deriveKeyPairECDSA(signatureScheme.algSpec as ECParameterSpec, privateKey, seed)
EDDSA_ED25519_SHA512 -> return deriveKeyPairEdDSA(privateKey, seed)
}
throw UnsupportedOperationException("Although supported for signing, deterministic key derivation is not currently implemented for ${signatureScheme.schemeCodeName}")
}
/**
* Deterministically generate/derive a [KeyPair] using an existing private key and a seed as inputs.
* Use this method if the [SignatureScheme] of the private key input is not known.
* @param privateKey the [PrivateKey] that will be used as key to the HMAC-ed DKG function.
* @param seed an extra seed that will be used as value to the underlying HMAC.
* @return a new deterministically generated [KeyPair].
* @throws IllegalArgumentException if the requested signature scheme is not supported.
* @throws UnsupportedOperationException if deterministic key generation is not supported for this particular scheme.
*/
fun deterministicKeyPair(privateKey: PrivateKey, seed: ByteArray): KeyPair {
return deterministicKeyPair(findSignatureScheme(privateKey), privateKey, seed)
}
// Given the domain parameters, this routine deterministically generates an ECDSA key pair
// in accordance with X9.62 section 5.2.1 pages 26, 27.
private fun deriveKeyPairECDSA(parameterSpec: ECParameterSpec, privateKey: PrivateKey, seed: ByteArray): KeyPair {
// Compute HMAC(privateKey, seed).
val macBytes = deriveHMAC(privateKey, seed)
// Get the first EC curve fieldSized-bytes from macBytes.
// According to recommendations from the deterministic ECDSA rfc, see https://tools.ietf.org/html/rfc6979
// performing a simple modular reduction would induce biases that would be detrimental to security.
// Thus, the result is not reduced modulo q and similarly to BIP32, EC curve fieldSized-bytes are utilised.
val fieldSizeMacBytes = macBytes.copyOf(parameterSpec.curve.fieldSize / 8)
// Calculate value d for private key.
val deterministicD = BigInteger(1, fieldSizeMacBytes)
// Key generation checks follow the BC logic found in
// https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/generators/ECKeyPairGenerator.java
// There is also an extra check to align with the BIP32 protocol, according to which
// if deterministicD >= order_of_the_curve the resulted key is invalid and we should proceed with another seed.
// TODO: We currently use SHA256(seed) when retrying, but BIP32 just skips a counter (i) that results to an invalid key.
// Although our hashing approach seems reasonable, we should check if there are alternatives,
// especially if we use counters as well.
if (deterministicD < ECConstants.TWO
|| WNafUtil.getNafWeight(deterministicD) < parameterSpec.n.bitLength().ushr(2)
|| deterministicD >= parameterSpec.n) {
// Instead of throwing an exception, we retry with SHA256(seed).
return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes)
}
val privateKeySpec = ECPrivateKeySpec(deterministicD, parameterSpec)
val privateKeyD = BCECPrivateKey(privateKey.algorithm, privateKeySpec, BouncyCastleProvider.CONFIGURATION)
// Compute the public key by scalar multiplication.
// Note that BIP32 uses masterKey + mac_derived_key as the final private key and it consequently
// requires an extra point addition: master_public + mac_derived_public for the public part.
// In our model, the mac_derived_output, deterministicD, is not currently added to the masterKey and it
// it forms, by itself, the new private key, which in turn is used to compute the new public key.
val pointQ = FixedPointCombMultiplier().multiply(parameterSpec.g, deterministicD)
// This is unlikely to happen, but we should check for point at infinity.
if (pointQ.isInfinity)
// Instead of throwing an exception, we retry with SHA256(seed).
return deriveKeyPairECDSA(parameterSpec, privateKey, seed.sha256().bytes)
val publicKeySpec = ECPublicKeySpec(pointQ, parameterSpec)
val publicKeyD = BCECPublicKey(privateKey.algorithm, publicKeySpec, BouncyCastleProvider.CONFIGURATION)
return KeyPair(publicKeyD, privateKeyD)
}
// Deterministically generate an EdDSA key.
private fun deriveKeyPairEdDSA(privateKey: PrivateKey, seed: ByteArray): KeyPair {
// Compute HMAC(privateKey, seed).
val macBytes = deriveHMAC(privateKey, seed)
// Calculate key pair.
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
val bytes = macBytes.copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
val privateKeyD = EdDSAPrivateKeySpec(bytes, params)
val publicKeyD = EdDSAPublicKeySpec(privateKeyD.a, params)
return KeyPair(EdDSAPublicKey(publicKeyD), EdDSAPrivateKey(privateKeyD))
}
/**
* Returns a key pair derived from the given [BigInteger] entropy. This is useful for unit tests
* and other cases where you want hard-coded private keys.
@ -535,11 +677,11 @@ object Crypto {
* @return a new [KeyPair] from an entropy input.
* @throws IllegalArgumentException if the requested signature scheme is not supported for KeyPair generation using an entropy input.
*/
fun generateKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
fun deriveKeyPairFromEntropy(signatureScheme: SignatureScheme, entropy: BigInteger): KeyPair {
when (signatureScheme) {
EDDSA_ED25519_SHA512 -> return generateEdDSAKeyPairFromEntropy(entropy)
EDDSA_ED25519_SHA512 -> return deriveEdDSAKeyPairFromEntropy(entropy)
}
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: $signatureScheme.schemeCodeName")
throw IllegalArgumentException("Unsupported signature scheme for fixed entropy-based key pair generation: ${signatureScheme.schemeCodeName}")
}
/**
@ -547,32 +689,51 @@ object Crypto {
* @param entropy a [BigInteger] value.
* @return a new [KeyPair] from an entropy input.
*/
fun generateKeyPairFromEntropy(entropy: BigInteger): KeyPair = generateKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
fun deriveKeyPairFromEntropy(entropy: BigInteger): KeyPair = deriveKeyPairFromEntropy(DEFAULT_SIGNATURE_SCHEME, entropy)
// custom key pair generator from entropy.
private fun generateEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
private fun deriveEdDSAKeyPairFromEntropy(entropy: BigInteger): KeyPair {
val params = EDDSA_ED25519_SHA512.algSpec as EdDSANamedCurveSpec
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // need to pad the entropy to the valid seed length.
val bytes = entropy.toByteArray().copyOf(params.curve.field.getb() / 8) // Need to pad the entropy to the valid seed length.
val priv = EdDSAPrivateKeySpec(bytes, params)
val pub = EdDSAPublicKeySpec(priv.a, params)
return KeyPair(EdDSAPublicKey(pub), EdDSAPrivateKey(priv))
}
// Compute the HMAC-SHA512 using a privateKey as the MAC_key and a seed ByteArray.
private fun deriveHMAC(privateKey: PrivateKey, seed: ByteArray): ByteArray {
// Compute hmac(privateKey, seed).
val mac = Mac.getInstance("HmacSHA512", providerMap[BouncyCastleProvider.PROVIDER_NAME])
val keyData = when (privateKey) {
is BCECPrivateKey -> privateKey.d.toByteArray()
is EdDSAPrivateKey -> privateKey.geta()
else -> throw InvalidKeyException("Key type ${privateKey.algorithm} is not supported for deterministic key derivation")
}
val key = SecretKeySpec(keyData, "HmacSHA512")
mac.init(key)
return mac.doFinal(seed)
}
/**
* Use bouncy castle utilities to sign completed X509 certificate with CA cert private key.
* Build a partial X.509 certificate ready for signing.
*
* @param issuer name of the issuing entity.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
fun createCertificate(certificateType: CertificateType, issuer: X500Name,
subject: X500Name, subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509Certificate {
nameConstraints: NameConstraints? = null): X509v3CertificateBuilder {
val signatureScheme = findSignatureScheme(issuerKeyPair.private)
val provider = providerMap[signatureScheme.providerName]
val serial = BigInteger.valueOf(random63BitValue())
val keyPurposes = DERSequence(ASN1EncodableVector().apply { certificateType.purposes.forEach { add(it) } })
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(ASN1Sequence.getInstance(subjectPublicKey.encoded))
val builder = JcaX509v3CertificateBuilder(issuer, serial, validityWindow.first, validityWindow.second, subject, subjectPublicKey)
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(SubjectPublicKeyInfo.getInstance(subjectPublicKey.encoded)))
.addExtension(Extension.subjectKeyIdentifier, false, BcX509ExtensionUtils().createSubjectKeyIdentifier(subjectPublicKeyInfo))
.addExtension(Extension.basicConstraints, certificateType.isCA, BasicConstraints(certificateType.isCA))
.addExtension(Extension.keyUsage, false, certificateType.keyUsage)
.addExtension(Extension.extendedKeyUsage, false, keyPurposes)
@ -580,11 +741,52 @@ object Crypto {
if (nameConstraints != null) {
builder.addExtension(Extension.nameConstraints, true, nameConstraints)
}
return builder
}
/**
* Build and sign an X.509 certificate with the given signer.
*
* @param issuer name of the issuing entity.
* @param issuerSigner content signer to sign the certificate with.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerSigner: ContentSigner,
subject: X500Name, subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
return builder.build(issuerSigner).apply {
require(isValidOn(Date()))
}
}
/**
* Build and sign an X.509 certificate with CA cert private key.
*
* @param issuer name of the issuing entity.
* @param issuerKeyPair the public & private key to sign the certificate with.
* @param subject name of the certificate subject.
* @param subjectPublicKey public key of the certificate subject.
* @param validityWindow the time period the certificate is valid for.
* @param nameConstraints any name constraints to impose on certificates signed by the generated certificate.
*/
fun createCertificate(certificateType: CertificateType, issuer: X500Name, issuerKeyPair: KeyPair,
subject: X500Name, subjectPublicKey: PublicKey,
validityWindow: Pair<Date, Date>,
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val signatureScheme = findSignatureScheme(issuerKeyPair.private)
val provider = providerMap[signatureScheme.providerName]
val builder = createCertificate(certificateType, issuer, subject, subjectPublicKey, validityWindow, nameConstraints)
val signer = ContentSignerBuilder.build(signatureScheme, issuerKeyPair.private, provider)
return JcaX509CertificateConverter().setProvider(provider).getCertificate(builder.build(signer)).apply {
checkValidity(Date())
verify(issuerKeyPair.public, provider)
return builder.build(signer).apply {
require(isValidOn(Date()))
require(isSignatureValid(JcaContentVerifierProviderBuilder().build(issuerKeyPair.public)))
}
}
@ -617,8 +819,7 @@ object Crypto {
*/
@Throws(IllegalArgumentException::class)
fun publicKeyOnCurve(signatureScheme: SignatureScheme, publicKey: PublicKey): Boolean {
if (!isSupportedSignatureScheme(signatureScheme))
throw IllegalArgumentException("Unsupported signature scheme: $signatureScheme.schemeCodeName")
require(isSupportedSignatureScheme(signatureScheme)) { "Unsupported key/algorithm for schemeCodeName: ${signatureScheme.schemeCodeName}" }
when (publicKey) {
is BCECPublicKey -> return (publicKey.parameters == signatureScheme.algSpec && !publicKey.q.isInfinity && publicKey.q.isValid)
is EdDSAPublicKey -> return (publicKey.params == signatureScheme.algSpec && !isEdDSAPointAtInfinity(publicKey) && publicKey.a.isOnCurve)
@ -671,6 +872,19 @@ object Crypto {
}
}
/**
* Convert a public key to a supported implementation. This method is usually required to retrieve a key from an
* [X509CertificateHolder].
*
* @param key a public key.
* @return a supported implementation of the input public key.
* @throws IllegalArgumentException on not supported scheme or if the given key specification
* is inappropriate for a supported key factory to produce a private key.
*/
fun toSupportedPublicKey(key: SubjectPublicKeyInfo): PublicKey {
return Crypto.decodePublicKey(key.encoded)
}
/**
* Convert a public key to a supported implementation. This can be used to convert a SUN's EC key to an BC key.
* This method is usually required to retrieve a key (via its corresponding cert) from JKS keystores that by default return SUN implementations.

View File

@ -2,7 +2,6 @@
package net.corda.core.crypto
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
@ -24,6 +23,7 @@ val NULL_PARTY = AnonymousParty(NullPublicKey)
// TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable
@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead")
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
@ -145,12 +145,12 @@ fun generateKeyPair(): KeyPair = Crypto.generateKeyPair()
* you want hard-coded private keys.
* This currently works for the default signature scheme EdDSA ed25519 only.
*/
fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.generateKeyPairFromEntropy(entropy)
fun entropyToKeyPair(entropy: BigInteger): KeyPair = Crypto.deriveKeyPairFromEntropy(entropy)
/**
* Helper function for signing.
* @param metaData tha attached MetaData object.
* @return a [TransactionSignature] object.
* @return a [TransactionSignature ] object.
* @throws IllegalArgumentException if the signature scheme is not supported for this private key.
* @throws InvalidKeyException if the private key is invalid.
* @throws SignatureException if signing is not possible due to malformed data or private key.

View File

@ -3,13 +3,14 @@ package net.corda.core.crypto
import net.corda.core.exists
import net.corda.core.read
import net.corda.core.write
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.path.CertPath
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.Path
import java.security.*
import java.security.cert.Certificate
import java.security.cert.X509Certificate
object KeyStoreUtilities {
val KEYSTORE_TYPE = "JKS"
@ -67,6 +68,18 @@ object KeyStoreUtilities {
}
}
/**
* Helper extension method to add, or overwrite any key data in store.
* @param alias name to record the private key and certificate chain under.
* @param key cryptographic key to store.
* @param password password for unlocking the key entry in the future. This does not have to be the same password as any keys stored,
* but for SSL purposes this is recommended.
* @param chain the sequence of certificates starting with the public key certificate for this key and extending to the root CA cert.
*/
fun KeyStore.addOrReplaceKey(alias: String, key: Key, password: CharArray, chain: CertPath) {
addOrReplaceKey(alias, key, password, chain.certificates.map { it.cert }.toTypedArray<Certificate>())
}
/**
* Helper extension method to add, or overwrite any key data in store.
* @param alias name to record the private key and certificate chain under.
@ -122,8 +135,9 @@ fun KeyStore.getKeyPair(alias: String, keyPassword: String): KeyPair = getCertif
* @param keyPassword The password for the PrivateKey (not the store access password).
*/
fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): CertificateAndKeyPair {
val cert = getCertificate(alias) as X509Certificate
return CertificateAndKeyPair(cert, KeyPair(Crypto.toSupportedPublicKey(cert.publicKey), getSupportedKey(alias, keyPassword)))
val cert = getX509Certificate(alias)
val publicKey = Crypto.toSupportedPublicKey(cert.subjectPublicKeyInfo)
return CertificateAndKeyPair(cert, KeyPair(publicKey, getSupportedKey(alias, keyPassword)))
}
/**
@ -131,7 +145,10 @@ fun KeyStore.getCertificateAndKeyPair(alias: String, keyPassword: String): Certi
* @param alias The name to lookup the Key and Certificate chain from.
* @return The X509Certificate found in the KeyStore under the specified alias.
*/
fun KeyStore.getX509Certificate(alias: String): X509Certificate = getCertificate(alias) as X509Certificate
fun KeyStore.getX509Certificate(alias: String): X509CertificateHolder {
val encoded = getCertificate(alias)?.encoded ?: throw IllegalArgumentException("No certificate under alias \"${alias}\"")
return X509CertificateHolder(encoded)
}
/**
* Extract a private key from a KeyStore file assuming storage alias is known.

View File

@ -8,7 +8,7 @@ import java.util.*
* See: https://en.wikipedia.org/wiki/Merkle_tree
*
* Transaction is split into following blocks: inputs, attachments' refs, outputs, commands, notary,
* signers, tx type, timestamp. Merkle Tree is kept in a recursive data structure. Building is done bottom up,
* signers, tx type, time-window. Merkle Tree is kept in a recursive data structure. Building is done bottom up,
* from all leaves' hashes. If number of leaves is not a power of two, the tree is padded with zero hashes.
*/
sealed class MerkleTree {

View File

@ -7,6 +7,7 @@ import org.bouncycastle.asn1.x500.X500NameBuilder
import org.bouncycastle.asn1.x500.style.BCStyle
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter
import org.bouncycastle.openssl.jcajce.JcaPEMWriter
import org.bouncycastle.util.io.pem.PemReader
import java.io.FileReader
@ -24,6 +25,7 @@ import java.time.temporal.ChronoUnit
import java.util.*
object X509Utilities {
val DEFAULT_IDENTITY_SIGNATURE_SCHEME = Crypto.EDDSA_ED25519_SHA512
val DEFAULT_TLS_SIGNATURE_SCHEME = Crypto.ECDSA_SECP256R1_SHA256
// Aliases for private keys and certificates.
@ -59,7 +61,7 @@ object X509Utilities {
* @param after duration to roll forward returned end date relative to current date.
* @param parent if provided certificate whose validity should bound the date interval returned.
*/
private fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509Certificate? = null): Pair<Date, Date> {
fun getCertificateValidityWindow(before: Duration, after: Duration, parent: X509CertificateHolder? = null): Pair<Date, Date> {
val startOfDayUTC = Instant.now().truncatedTo(ChronoUnit.DAYS)
val notBefore = max(startOfDayUTC - before, parent?.notBefore)
val notAfter = min(startOfDayUTC + after, parent?.notAfter)
@ -76,7 +78,7 @@ object X509Utilities {
nameBuilder.addRDN(BCStyle.O, "R3")
nameBuilder.addRDN(BCStyle.OU, "corda")
nameBuilder.addRDN(BCStyle.L, "London")
nameBuilder.addRDN(BCStyle.C, "UK")
nameBuilder.addRDN(BCStyle.C, "GB")
return nameBuilder.build()
}
@ -101,10 +103,9 @@ object X509Utilities {
* Create a de novo root self-signed X509 v3 CA cert.
*/
@JvmStatic
fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509Certificate {
fun createSelfSignedCACertificate(subject: X500Name, keyPair: KeyPair, validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW): X509CertificateHolder {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second)
val cert = Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window)
return cert
return Crypto.createCertificate(CertificateType.ROOT_CA, subject, keyPair, subject, keyPair.public, window)
}
/**
@ -119,34 +120,18 @@ object X509Utilities {
*/
@JvmStatic
fun createCertificate(certificateType: CertificateType,
issuerCertificate: X509Certificate, issuerKeyPair: KeyPair,
issuerCertificate: X509CertificateHolder, issuerKeyPair: KeyPair,
subject: X500Name, subjectPublicKey: PublicKey,
validityWindow: Pair<Duration, Duration> = DEFAULT_VALIDITY_WINDOW,
nameConstraints: NameConstraints? = null): X509Certificate {
nameConstraints: NameConstraints? = null): X509CertificateHolder {
val window = getCertificateValidityWindow(validityWindow.first, validityWindow.second, issuerCertificate)
val cert = Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints)
return cert
return Crypto.createCertificate(certificateType, issuerCertificate.subject, issuerKeyPair, subject, subjectPublicKey, window, nameConstraints)
}
/**
* Build a certificate path from a trusted root certificate to a target certificate. This will always return a path
* directly from the target to the root.
*
* @param trustedRoot trusted root certificate that will be the start of the path.
* @param certificates certificates in the path.
* @param revocationEnabled whether revocation of certificates in the path should be checked.
*/
fun createCertificatePath(trustedRoot: X509Certificate, vararg certificates: X509Certificate, revocationEnabled: Boolean): CertPath {
val certFactory = CertificateFactory.getInstance("X509")
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
params.isRevocationEnabled = revocationEnabled
return certFactory.generateCertPath(certificates.toList())
}
fun validateCertificateChain(trustedRoot: X509Certificate, vararg certificates: Certificate) {
fun validateCertificateChain(trustedRoot: X509CertificateHolder, vararg certificates: Certificate) {
require(certificates.isNotEmpty()) { "Certificate path must contain at least one certificate" }
val certFactory = CertificateFactory.getInstance("X509")
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot, null)))
val params = PKIXParameters(setOf(TrustAnchor(trustedRoot.cert, null)))
params.isRevocationEnabled = false
val certPath = certFactory.generateCertPath(certificates.toList())
val pathValidator = CertPathValidator.getInstance("PKIX")
@ -159,7 +144,7 @@ object X509Utilities {
* @param filename Target filename.
*/
@JvmStatic
fun saveCertificateAsPEMFile(x509Certificate: X509Certificate, filename: Path) {
fun saveCertificateAsPEMFile(x509Certificate: X509CertificateHolder, filename: Path) {
FileWriter(filename.toFile()).use {
JcaPEMWriter(it).use {
it.writeObject(x509Certificate)
@ -173,11 +158,12 @@ object X509Utilities {
* @return The X509Certificate that was encoded in the file.
*/
@JvmStatic
fun loadCertificateFromPEMFile(filename: Path): X509Certificate {
fun loadCertificateFromPEMFile(filename: Path): X509CertificateHolder {
val reader = PemReader(FileReader(filename.toFile()))
val pemObject = reader.readPemObject()
return CertificateStream(pemObject.content.inputStream()).nextCertificate().apply {
checkValidity()
val cert = X509CertificateHolder(pemObject.content)
return cert.apply {
isValidOn(Date())
}
}
@ -218,7 +204,7 @@ object X509Utilities {
CORDA_CLIENT_CA,
clientKey.private,
keyPass,
arrayOf(clientCACert, intermediateCACert, rootCACert))
org.bouncycastle.cert.path.CertPath(arrayOf(clientCACert, intermediateCACert, rootCACert)))
clientCAKeystore.save(clientCAKeystorePath, storePassword)
val tlsKeystore = KeyStoreUtilities.loadOrCreateKeyStore(sslKeyStorePath, storePassword)
@ -226,7 +212,7 @@ object X509Utilities {
CORDA_CLIENT_TLS,
tlsKey.private,
keyPass,
arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert))
org.bouncycastle.cert.path.CertPath(arrayOf(clientTLSCert, clientCACert, intermediateCACert, rootCACert)))
tlsKeystore.save(sslKeyStorePath, storePassword)
}
@ -275,7 +261,9 @@ private fun X500Name.mutateCommonName(mutator: (ASN1Encodable) -> String): X500N
val X500Name.commonName: String get() = getRDNs(BCStyle.CN).first().first.value.toString()
val X500Name.orgName: String? get() = getRDNs(BCStyle.O).firstOrNull()?.first?.value?.toString()
val X500Name.location: String get() = getRDNs(BCStyle.L).first().first.value.toString()
val X500Name.locationOrNull: String? get() = try { location } catch (e: Exception) { null }
val X509Certificate.subject: X500Name get() = X509CertificateHolder(encoded).subject
val X509CertificateHolder.cert: X509Certificate get() = JcaX509CertificateConverter().getCertificate(this)
class CertificateStream(val input: InputStream) {
private val certificateFactory = CertificateFactory.getInstance("X.509")
@ -283,12 +271,13 @@ class CertificateStream(val input: InputStream) {
fun nextCertificate(): X509Certificate = certificateFactory.generateCertificate(input) as X509Certificate
}
data class CertificateAndKeyPair(val certificate: X509Certificate, val keyPair: KeyPair)
data class CertificateAndKeyPair(val certificate: X509CertificateHolder, val keyPair: KeyPair)
enum class CertificateType(val keyUsage: KeyUsage, vararg val purposes: KeyPurposeId, val isCA: Boolean) {
ROOT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
INTERMEDIATE_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
CLIENT_CA(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign or KeyUsage.cRLSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true),
TLS(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment or KeyUsage.keyAgreement), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false),
IDENTITY(KeyUsage(KeyUsage.digitalSignature), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = false)
}
// TODO: Identity certs should have only limited depth (i.e. 1) CA signing capability, with tight name constraints
IDENTITY(KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyCertSign), KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth, KeyPurposeId.anyExtendedKeyUsage, isCA = true)
}

View File

@ -1,7 +1,9 @@
package net.corda.core.flows
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.CordaException
import net.corda.core.utilities.CordaRuntimeException
// DOCSTART 1
/**
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
@ -11,17 +13,18 @@ import net.corda.core.serialization.CordaSerializable
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
* It is recommended a [FlowLogic] document the [FlowException] types it can throw.
*/
@CordaSerializable
open class FlowException(override val message: String?, override val cause: Throwable?) : Exception() {
open class FlowException(message: String?, cause: Throwable?) : CordaException(message, cause) {
constructor(message: String?) : this(message, null)
constructor(cause: Throwable?) : this(cause?.toString(), cause)
constructor() : this(null, null)
}
// DOCEND 1
/**
* Thrown when a flow session ends unexpectedly due to a type mismatch (the other side sent an object of a type
* that we were not expecting), or the other side had an internal error, or the other side terminated when we
* were waiting for a response.
*/
@CordaSerializable
class FlowSessionException(message: String) : RuntimeException(message)
class FlowSessionException(message: String?, cause: Throwable?) : CordaRuntimeException(message, cause) {
constructor(msg: String) : this(msg, null)
}

View File

@ -0,0 +1,31 @@
package net.corda.core.flows
import net.corda.core.contracts.ScheduledStateRef
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import java.security.Principal
/**
* FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC],
* communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled]
* or via the Corda Shell [FlowInitiator.Shell].
*/
@CordaSerializable
sealed class FlowInitiator : Principal {
/** Started using [net.corda.core.messaging.CordaRPCOps.startFlowDynamic]. */
data class RPC(val username: String) : FlowInitiator() {
override fun getName(): String = username
}
/** Started when we get new session initiation request. */
data class Peer(val party: Party) : FlowInitiator() {
override fun getName(): String = party.name.toString()
}
/** Started as scheduled activity. */
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() {
override fun getName(): String = "Scheduler"
}
// TODO When proper ssh access enabled, add username/use RPC?
object Shell : FlowInitiator() {
override fun getName(): String = "Shell User"
}
}

View File

@ -3,6 +3,7 @@ package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker

View File

@ -1,81 +0,0 @@
package net.corda.core.flows
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.ScheduledStateRef
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
import java.security.Principal
import java.util.*
/**
* FlowInitiator holds information on who started the flow. We have different ways of doing that: via RPC [FlowInitiator.RPC],
* communication started by peer node [FlowInitiator.Peer], scheduled flows [FlowInitiator.Scheduled]
* or via the Corda Shell [FlowInitiator.Shell].
*/
@CordaSerializable
sealed class FlowInitiator : Principal {
/** Started using [net.corda.core.messaging.CordaRPCOps.startFlowDynamic]. */
data class RPC(val username: String) : FlowInitiator() {
override fun getName(): String = username
}
/** Started when we get new session initiation request. */
data class Peer(val party: Party) : FlowInitiator() {
override fun getName(): String = party.name.toString()
}
/** Started as scheduled activity. */
data class Scheduled(val scheduledState: ScheduledStateRef) : FlowInitiator() {
override fun getName(): String = "Scheduler"
}
// TODO When proper ssh access enabled, add username/use RPC?
object Shell : FlowInitiator() {
override fun getName(): String = "Shell User"
}
}
/**
* A unique identifier for a single state machine run, valid across node restarts. Note that a single run always
* has at least one flow, but that flow may also invoke sub-flows: they all share the same run id.
*/
@CordaSerializable
data class StateMachineRunId(val uuid: UUID) {
companion object {
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
}
override fun toString(): String = "[$uuid]"
}
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
interface FlowStateMachine<R> {
@Suspendable
fun <T : Any> sendAndReceive(receiveType: Class<T>,
otherParty: Party,
payload: Any,
sessionFlow: FlowLogic<*>,
retrySend: Boolean = false): UntrustworthyData<T>
@Suspendable
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
@Suspendable
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
@Suspendable
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>)
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>)
val serviceHub: ServiceHub
val logger: Logger
val id: StateMachineRunId
val resultFuture: ListenableFuture<R>
val flowInitiator: FlowInitiator
}

View File

@ -0,0 +1,16 @@
package net.corda.core.flows
import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.reflect.KClass
/**
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that
* does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow].
*
* The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating
* to initiated flow mapping.
*
* @see InitiatingFlow
*/
@Target(CLASS)
annotation class InitiatedBy(val value: KClass<out FlowLogic<*>>)

View File

@ -5,8 +5,7 @@ import kotlin.annotation.AnnotationTarget.CLASS
/**
* This annotation is required by any [FlowLogic] which has been designated to initiate communication with a counterparty
* and request they start their side of the flow communication. To ensure that this is correctly applied
* [net.corda.core.node.PluginServiceHub.registerServiceFlow] checks the initiating flow class has this annotation.
* and request they start their side of the flow communication.
*
* There is also an optional [version] property, which defaults to 1, to specify the version of the flow protocol. This
* integer value should be incremented whenever there is a release of this flow which has changes that are not backwards
@ -19,6 +18,11 @@ import kotlin.annotation.AnnotationTarget.CLASS
*
* The flow version number is similar in concept to Corda's platform version but they are not the same. A flow's version
* number can change independently of the platform version.
*
* If you are customising an existing initiating flow by sub-classing it then there's no need to specify this annotation
* again. In fact doing so is an error and checks are made to make sure this doesn't occur.
*
* @see InitiatedBy
*/
// TODO Add support for multiple versions once CorDapps are loaded in separate class loaders
@Target(CLASS)

View File

@ -1,6 +1,5 @@
package net.corda.core.flows
import java.lang.annotation.Inherited
import kotlin.annotation.AnnotationTarget.CLASS
/**
@ -9,7 +8,6 @@ import kotlin.annotation.AnnotationTarget.CLASS
* flow will not be allowed to start and an exception will be thrown.
*/
@Target(CLASS)
@Inherited
@MustBeDocumented
// TODO Consider a different name, something along the lines of SchedulableFlow
annotation class StartableByRPC

View File

@ -0,0 +1,17 @@
package net.corda.core.flows
import net.corda.core.serialization.CordaSerializable
import java.util.*
/**
* A unique identifier for a single state machine run, valid across node restarts. Note that a single run always
* has at least one flow, but that flow may also invoke sub-flows: they all share the same run id.
*/
@CordaSerializable
data class StateMachineRunId(val uuid: UUID) {
companion object {
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
}
override fun toString(): String = "[$uuid]"
}

View File

@ -18,6 +18,15 @@ abstract class AbstractParty(val owningKey: PublicKey) {
override fun hashCode(): Int = owningKey.hashCode()
abstract fun nameOrNull(): X500Name?
/**
* Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
* ledger.
*/
abstract fun ref(bytes: OpaqueBytes): PartyAndReference
/**
* Build a reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
* ledger.
*/
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
}

View File

@ -3,6 +3,7 @@ package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.toBase58String
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey
@ -14,7 +15,7 @@ import java.security.PublicKey
* cryptographic public key primitives into a tree structure.
*
* For example: Alice has two key pairs (pub1/priv1 and pub2/priv2), and wants to be able to sign transactions with either of them.
* Her advertised [Party] then has a legal X.500 [name] "CN=Alice Corp,O=Alice Corp,L=London,C=UK" and an [owningKey]
* Her advertised [Party] then has a legal X.500 [name] "CN=Alice Corp,O=Alice Corp,L=London,C=GB" and an [owningKey]
* "pub1 or pub2".
*
* [Party] is also used for service identities. E.g. Alice may also be running an interest rate oracle on her Corda node,
@ -27,7 +28,7 @@ import java.security.PublicKey
* @see CompositeKey
*/
class Party(val name: X500Name, owningKey: PublicKey) : AbstractParty(owningKey) {
constructor(certAndKey: CertificateAndKeyPair) : this(X500Name(certAndKey.certificate.subjectDN.name), certAndKey.keyPair.public)
constructor(certAndKey: CertificateAndKeyPair) : this(certAndKey.certificate.subject, certAndKey.keyPair.public)
override fun toString() = name.toString()
override fun nameOrNull(): X500Name? = name

View File

@ -0,0 +1,33 @@
package net.corda.core.identity
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey
import java.security.cert.CertPath
/**
* A full party plus the X.509 certificate and path linking the party back to a trust root. Equality of
* [PartyAndCertificate] instances is based on the party only, as certificate and path are data associated with the party,
* not part of the identifier themselves.
*/
@CordaSerializable
data class PartyAndCertificate(val party: Party,
val certificate: X509CertificateHolder,
val certPath: CertPath) {
constructor(name: X500Name, owningKey: PublicKey, certificate: X509CertificateHolder, certPath: CertPath) : this(Party(name, owningKey), certificate, certPath)
val name: X500Name
get() = party.name
val owningKey: PublicKey
get() = party.owningKey
override fun equals(other: Any?): Boolean {
return if (other is PartyAndCertificate)
party == other.party
else
false
}
override fun hashCode(): Int = party.hashCode()
override fun toString(): String = party.toString()
}

View File

@ -0,0 +1,42 @@
package net.corda.core.internal
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.UntrustworthyData
import org.slf4j.Logger
/** This is an internal interface that is implemented by code in the node module. You should look at [FlowLogic]. */
interface FlowStateMachine<R> {
@Suspendable
fun <T : Any> sendAndReceive(receiveType: Class<T>,
otherParty: Party,
payload: Any,
sessionFlow: FlowLogic<*>,
retrySend: Boolean = false): UntrustworthyData<T>
@Suspendable
fun <T : Any> receive(receiveType: Class<T>, otherParty: Party, sessionFlow: FlowLogic<*>): UntrustworthyData<T>
@Suspendable
fun send(otherParty: Party, payload: Any, sessionFlow: FlowLogic<*>)
@Suspendable
fun waitForLedgerCommit(hash: SecureHash, sessionFlow: FlowLogic<*>): SignedTransaction
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>)
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>)
val serviceHub: ServiceHub
val logger: Logger
val id: StateMachineRunId
val resultFuture: ListenableFuture<R>
val flowInitiator: FlowInitiator
}

View File

@ -244,6 +244,16 @@ interface CordaRPCOps : RPCOps {
*/
fun partyFromX500Name(x500Name: X500Name): Party?
/**
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
* get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results
* but rather show them via a user interface and let the user pick the one they wanted.
*
* @param query The string to check against the X.500 name components
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
*/
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
/** Enumerates the class names of the flows that this node knows about. */
fun registeredFlows(): List<String>
}

View File

@ -40,8 +40,8 @@ interface FlowProgressHandle<A> : FlowHandle<A> {
@CordaSerializable
data class FlowHandleImpl<A>(
override val id: StateMachineRunId,
override val returnValue: ListenableFuture<A>) : FlowHandle<A> {
override val id: StateMachineRunId,
override val returnValue: ListenableFuture<A>) : FlowHandle<A> {
// Remember to add @Throws to FlowHandle.close() if this throws an exception.
override fun close() {
@ -51,9 +51,9 @@ data class FlowHandleImpl<A>(
@CordaSerializable
data class FlowProgressHandleImpl<A>(
override val id: StateMachineRunId,
override val returnValue: ListenableFuture<A>,
override val progress: Observable<String>) : FlowProgressHandle<A> {
override val id: StateMachineRunId,
override val returnValue: ListenableFuture<A>,
override val progress: Observable<String>) : FlowProgressHandle<A> {
// Remember to add @Throws to FlowProgressHandle.close() if this throws an exception.
override fun close() {

View File

@ -9,17 +9,16 @@ import java.util.function.Function
* to extend a Corda node with additional application services.
*/
abstract class CordaPluginRegistry {
/**
* List of lambdas returning JAX-RS objects. They may only depend on the RPC interface, as the webserver should
* potentially be able to live in a process separate from the node itself.
*/
@Suppress("unused")
@Deprecated("This is no longer in use, moved to WebServerPluginRegistry class in webserver module",
level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("net.corda.webserver.services.WebServerPluginRegistry"))
open val webApis: List<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()
*/
@Suppress("unused")
@Deprecated("This is no longer in use, moved to WebServerPluginRegistry class in webserver module",
level = DeprecationLevel.ERROR, replaceWith = ReplaceWith("net.corda.webserver.services.WebServerPluginRegistry"))
open val staticServeDirs: Map<String, String> get() = emptyMap()
@Suppress("unused")
@ -33,6 +32,9 @@ abstract class CordaPluginRegistry {
* The [PluginServiceHub] will be fully constructed before the plugin service is created and will
* allow access to the Flow factory and Flow initiation entry points there.
*/
@Suppress("unused")
@Deprecated("This is no longer used. If you need to create your own service, such as an oracle, then use the " +
"@CordaService annotation. For flow registrations use @InitiatedBy.", level = DeprecationLevel.ERROR)
open val servicePlugins: List<Function<PluginServiceHub, out Any>> get() = emptyList()
/**

View File

@ -1,31 +1,41 @@
package net.corda.core.node
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.cert.X509CertificateHolder
/**
* Information for an advertised service including the service specific identity information.
* The identity can be used in flows and is distinct from the Node's legalIdentity
*/
@CordaSerializable
data class ServiceEntry(val info: ServiceInfo, val identity: Party)
data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate)
/**
* Info about a network node that acts on behalf of some form of contract party.
*/
@CordaSerializable
data class NodeInfo(val address: SingleMessageRecipient,
val legalIdentity: Party,
val legalIdentityAndCert: PartyAndCertificate,
val platformVersion: Int,
var advertisedServices: List<ServiceEntry> = emptyList(),
val physicalLocation: PhysicalLocation? = null) {
init {
require(advertisedServices.none { it.identity == legalIdentity }) { "Service identities must be different from node legal identity" }
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
}
val notaryIdentity: Party get() = advertisedServices.single { it.info.type.isNotary() }.identity
fun serviceIdentities(type: ServiceType): List<Party> = advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
val legalIdentity: Party
get() = legalIdentityAndCert.party
val notaryIdentity: Party
get() = advertisedServices.single { it.info.type.isNotary() }.identity.party
fun serviceIdentities(type: ServiceType): List<Party> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
}
fun servideIdentitiesAndCert(type: ServiceType): List<PartyAndCertificate> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
}
}

View File

@ -7,22 +7,7 @@ import net.corda.core.identity.Party
* A service hub to be used by the [CordaPluginRegistry]
*/
interface PluginServiceHub : ServiceHub {
/**
* Register the service flow factory to use when an initiating party attempts to communicate with us. The registration
* is done against the [Class] object of the client flow to the service flow. What this means is if a counterparty
* starts a [FlowLogic] represented by [initiatingFlowClass] and starts communication with us, we will execute the service
* flow produced by [serviceFlowFactory]. This service flow has respond correctly to the sends and receives the client
* does.
* @param initiatingFlowClass [Class] of the client flow involved in this client-server communication.
* @param serviceFlowFactory Lambda which produces a new service flow for each new client flow communication. The
* [Party] parameter of the factory is the client's identity.
* @throws IllegalArgumentException If [initiatingFlowClass] is not annotated with [net.corda.core.flows.InitiatingFlow].
*/
fun registerServiceFlow(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>)
@Suppress("UNCHECKED_CAST")
@Deprecated("This is scheduled to be removed in a future release", ReplaceWith("registerServiceFlow"))
fun registerFlowInitiator(markerClass: Class<*>, flowFactory: (Party) -> FlowLogic<*>) {
registerServiceFlow(markerClass as Class<out FlowLogic<*>>, flowFactory)
}
@Deprecated("This is no longer used. Instead annotate the flows produced by your factory with @InitiatedBy and have " +
"them point to the initiating flow class.", level = DeprecationLevel.ERROR)
fun registerFlowInitiator(initiatingFlowClass: Class<out FlowLogic<*>>, serviceFlowFactory: (Party) -> FlowLogic<*>) = Unit
}

View File

@ -3,6 +3,7 @@ package net.corda.core.node
import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.node.services.*
import net.corda.core.serialization.SerializeAsToken
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
@ -44,6 +45,13 @@ interface ServiceHub : ServicesForResolution {
val clock: Clock
val myInfo: NodeInfo
/**
* Return the singleton instance of the given Corda service type. This is a class that is annotated with
* [CordaService] and will have automatically been registered by the node.
* @throws IllegalArgumentException If [type] is not annotated with [CordaService] or if the instance is not found.
*/
fun <T : SerializeAsToken> cordaService(type: Class<T>): T
/**
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then
* sends them to the vault for further processing. Expects to be run within a database transaction.
@ -141,7 +149,7 @@ interface ServiceHub : ServicesForResolution {
* @throws IllegalArgumentException is thrown if any keys are unavailable locally.
* @return Returns a [SignedTransaction] with the new node signature attached.
*/
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: List<PublicKey>): SignedTransaction {
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable<PublicKey>): SignedTransaction {
var stx: SignedTransaction? = null
for (pubKey in signingPubKeys) {
stx = if (stx == null) {

View File

@ -0,0 +1,22 @@
package net.corda.core.node.services
import kotlin.annotation.AnnotationTarget.CLASS
/**
* Annotate any class that needs to be a long-lived service within the node, such as an oracle, with this annotation.
* Such a class needs to have a constructor with a single parameter of type [net.corda.core.node.PluginServiceHub]. This
* construtor will be invoked during node start to initialise the service. The service hub provided can be used to get
* information about the node that may be necessary for the service. Corda services are created as singletons within
* the node and are available to flows via [net.corda.core.node.ServiceHub.cordaService].
*
* The service class has to implement [net.corda.core.serialization.SerializeAsToken] to ensure correct usage within flows.
* (If possible extend [net.corda.core.serialization.SingletonSerializeAsToken] instead as it removes the boilerplate.)
*
* The annotated class should expose its [ServiceType] via a public static field named `type`, so that the service is
* only loaded in nodes that declare the type in their advertisedServices.
*/
// TODO Handle the singleton serialisation of Corda services automatically, removing the need to implement SerializeAsToken
// TODO Perhaps this should be an interface or abstract class due to the need for it to implement SerializeAsToken and
// the need for the service type (which can be exposed by a simple getter)
@Target(CLASS)
annotation class CordaService

View File

@ -1,35 +1,41 @@
package net.corda.core.node.services
import net.corda.core.contracts.PartyAndReference
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.identity.*
import net.corda.core.node.NodeInfo
import org.bouncycastle.asn1.x500.X500Name
import java.security.InvalidAlgorithmParameterException
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.security.cert.CertificateExpiredException
import java.security.cert.CertificateNotYetValidException
/**
* An identity service maintains an bidirectional map of [Party]s to their associated public keys and thus supports
* lookup of a party given its key. This is obviously very incomplete and does not reflect everything a real identity
* service would provide.
* An identity service maintains a directory of parties by their associated distinguished name/public keys and thus
* supports lookup of a party given its key, or name. The service also manages the certificates linking confidential
* identities back to the well known identity (i.e. the identity in the network map) of a party.
*/
interface IdentityService {
fun registerIdentity(party: Party)
/**
* Verify and then store a well known identity.
*
* @param party a party representing a legal entity.
* @throws IllegalArgumentException if the certificate path is invalid, or if there is already an existing
* certificate chain for the anonymous party.
*/
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
fun registerIdentity(party: PartyAndCertificate)
/**
* Verify and then store the certificates proving that an anonymous party's key is owned by the given full
* party.
* Verify and then store an identity.
*
* @param trustedRoot trusted root certificate, typically the R3 master signing certificate.
* @param anonymousParty an anonymised party belonging to the legal entity.
* @param path certificate path from the trusted root to the anonymised party.
* @throws IllegalArgumentException if the chain does not link the two parties, or if there is already an existing
* certificate chain for the anonymous party. Anonymous parties must always resolve to a single owning party.
* @param anonymousParty a party representing a legal entity in a transaction.
* @param path certificate path from the trusted root to the party.
* @throws IllegalArgumentException if the certificate path is invalid, or if there is already an existing
* certificate chain for the anonymous party.
*/
// TODO: Move this into internal identity service once available
@Throws(IllegalArgumentException::class)
fun registerPath(trustedRoot: X509Certificate, anonymousParty: AnonymousParty, path: CertPath)
@Throws(CertificateExpiredException::class, CertificateNotYetValidException::class, InvalidAlgorithmParameterException::class)
fun registerAnonymousIdentity(anonymousParty: AnonymousParty, party: Party, path: CertPath)
/**
* Asserts that an anonymous party maps to the given full party, by looking up the certificate chain associated with
@ -44,24 +50,63 @@ interface IdentityService {
* Get all identities known to the service. This is expensive, and [partyFromKey] or [partyFromX500Name] should be
* used in preference where possible.
*/
fun getAllIdentities(): Iterable<Party>
fun getAllIdentities(): Iterable<PartyAndCertificate>
/**
* Get the certificate and path for a well known identity.
*
* @return the party and certificate, or null if unknown.
*/
fun certificateFromParty(party: Party): PartyAndCertificate?
// There is no method for removing identities, as once we are made aware of a Party we want to keep track of them
// indefinitely. It may be that in the long term we need to drop or archive very old Party information for space,
// but for now this is not supported.
fun partyFromKey(key: PublicKey): Party?
@Deprecated("Use partyFromX500Name")
@Deprecated("Use partyFromX500Name or partiesFromName")
fun partyFromName(name: String): Party?
fun partyFromX500Name(principal: X500Name): Party?
/**
* Resolve the well known identity of a party. If the party passed in is already a well known identity
* (i.e. a [Party]) this returns it as-is.
*
* @return the well known identity, or null if unknown.
*/
fun partyFromAnonymous(party: AbstractParty): Party?
/**
* Resolve the well known identity of a party. If the party passed in is already a well known identity
* (i.e. a [Party]) this returns it as-is.
*
* @return the well known identity, or null if unknown.
*/
fun partyFromAnonymous(partyRef: PartyAndReference) = partyFromAnonymous(partyRef.party)
/**
* Resolve the well known identity of a party. Throws an exception if the party cannot be identified.
* If the party passed in is already a well known identity (i.e. a [Party]) this returns it as-is.
*
* @return the well known identity.
* @throws IllegalArgumentException
*/
fun requirePartyFromAnonymous(party: AbstractParty): Party
/**
* Get the certificate chain showing an anonymous party is owned by the given party.
*/
fun pathForAnonymous(anonymousParty: AnonymousParty): CertPath?
/**
* Returns a list of candidate matches for a given string, with optional fuzzy(ish) matching. Fuzzy matching may
* get smarter with time e.g. to correct spelling errors, so you should not hard-code indexes into the results
* but rather show them via a user interface and let the user pick the one they wanted.
*
* @param query The string to check against the X.500 name components
* @param exactMatch If true, a case sensitive match is done against each component of each X.500 name.
*/
fun partiesFromName(query: String, exactMatch: Boolean): Set<Party>
class UnknownAnonymousPartyException(msg: String) : Exception(msg)
}

View File

@ -1,6 +1,7 @@
package net.corda.core.node.services
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceEntry
@ -8,10 +9,10 @@ import net.corda.core.node.ServiceEntry
* Holds information about a [Party], which may refer to either a specific node or a service.
*/
sealed class PartyInfo {
abstract val party: Party
abstract val party: PartyAndCertificate
data class Node(val node: NodeInfo) : PartyInfo() {
override val party get() = node.legalIdentity
override val party get() = node.legalIdentityAndCert
}
data class Service(val service: ServiceEntry) : PartyInfo() {

View File

@ -3,11 +3,14 @@ package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.*
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.keys
import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
@ -17,9 +20,12 @@ import net.corda.core.toFuture
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import org.bouncycastle.cert.X509CertificateHolder
import rx.Observable
import java.io.InputStream
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.time.Instant
import java.util.*
@ -357,12 +363,6 @@ inline fun <reified T : LinearState> VaultService.linearHeadsOfType() =
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED))
.associateBy { it.state.data.linearId }.mapValues { it.value }
// TODO: Remove this from the interface
// @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(LinearStateQueryCriteria(dealPartyName = listOf(<String>)))"))
inline fun <reified T : DealState> VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType<T>().values.filter {
it.state.data.parties.any { it == party }
}
class StatesNotAvailableException(override val message: String?, override val cause: Throwable? = null) : FlowException(message, cause) {
override fun toString() = "Soft locking error: $message"
}
@ -385,6 +385,17 @@ interface KeyManagementService {
@Suspendable
fun freshKey(): PublicKey
/**
* Generates a new random [KeyPair], adds it to the internal key storage, then generates a corresponding
* [X509Certificate] and adds it to the identity service.
*
* @param identity identity to generate a key and certificate for. Must be an identity this node has CA privileges for.
* @param revocationEnabled whether to check revocation status of certificates in the certificate path.
* @return X.509 certificate and path to the trust root.
*/
@Suspendable
fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath>
/** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data.
* @param bytes The data to sign over using the chosen key.
* @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
@ -398,10 +409,10 @@ interface KeyManagementService {
fun sign(bytes: ByteArray, publicKey: PublicKey): DigitalSignature.WithKey
}
// TODO: Move to a more appropriate location
/**
* An interface that denotes a service that can accept file uploads.
*/
// TODO This is no longer used and can be removed
interface FileUploader {
/**
* Accepts the data in the given input stream, and returns some sort of useful return message that will be sent

View File

@ -0,0 +1,26 @@
package net.corda.core.node.services
import net.corda.core.contracts.TimeWindow
import net.corda.core.seconds
import net.corda.core.until
import java.time.Clock
import java.time.Duration
/**
* Checks if the given time-window falls within the allowed tolerance interval.
*/
class TimeWindowChecker(val clock: Clock = Clock.systemUTC(),
val tolerance: Duration = 30.seconds) {
fun isValid(timeWindow: TimeWindow): Boolean {
val untilTime = timeWindow.untilTime
val fromTime = timeWindow.fromTime
val now = clock.instant()
// We don't need to test for (fromTime == null && untilTime == null) or backwards bounds because the TimeWindow
// constructor already checks that.
if (untilTime != null && untilTime until now > tolerance) return false
if (fromTime != null && now until fromTime > tolerance) return false
return true
}
}

View File

@ -1,26 +0,0 @@
package net.corda.core.node.services
import net.corda.core.contracts.Timestamp
import net.corda.core.seconds
import net.corda.core.until
import java.time.Clock
import java.time.Duration
/**
* Checks if the given timestamp falls within the allowed tolerance interval.
*/
class TimestampChecker(val clock: Clock = Clock.systemUTC(),
val tolerance: Duration = 30.seconds) {
fun isValid(timestampCommand: Timestamp): Boolean {
val before = timestampCommand.before
val after = timestampCommand.after
val now = clock.instant()
// We don't need to test for (before == null && after == null) or backwards bounds because the TimestampCommand
// constructor already checks that.
if (before != null && before until now > tolerance) return false
if (after != null && now until after > tolerance) return false
return true
}
}

View File

@ -1,6 +1,5 @@
package net.corda.core.node.services.vault
import net.corda.core.contracts.Commodity
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier

View File

@ -0,0 +1,125 @@
package net.corda.core.serialization
import sun.misc.Unsafe
import sun.security.util.Password
import java.io.*
import java.lang.invoke.*
import java.lang.reflect.*
import java.net.*
import java.security.*
import java.sql.Connection
import java.util.*
import java.util.logging.Handler
import java.util.zip.ZipFile
import kotlin.collections.HashSet
import kotlin.collections.LinkedHashSet
/**
* This is a [ClassWhitelist] implementation where everything is whitelisted except for blacklisted classes and interfaces.
* In practice, as flows are arbitrary code in which it is convenient to do many things,
* we can often end up pulling in a lot of objects that do not make sense to put in a checkpoint.
* Thus, by blacklisting classes/interfaces we don't expect to be serialised, we can better handle/monitor the aforementioned behaviour.
* Inheritance works for blacklisted items, but one can specifically exclude classes from blacklisting as well.
*/
object AllButBlacklisted : ClassWhitelist {
private val blacklistedClasses = hashSetOf<String>(
// Known blacklisted classes.
Thread::class.java.name,
HashSet::class.java.name,
HashMap::class.java.name,
ClassLoader::class.java.name,
Handler::class.java.name, // MemoryHandler, StreamHandler
Runtime::class.java.name,
Unsafe::class.java.name,
ZipFile::class.java.name,
Provider::class.java.name,
SecurityManager::class.java.name,
Random::class.java.name,
// Known blacklisted interfaces.
Connection::class.java.name,
// TODO: AutoCloseable::class.java.name,
// java.security.
KeyStore::class.java.name,
Password::class.java.name,
AccessController::class.java.name,
Permission::class.java.name,
// java.net.
DatagramSocket::class.java.name,
ServerSocket::class.java.name,
Socket::class.java.name,
URLConnection::class.java.name,
// TODO: add more from java.net.
// java.io.
Console::class.java.name,
File::class.java.name,
FileDescriptor::class.java.name,
FilePermission::class.java.name,
RandomAccessFile::class.java.name,
Reader::class.java.name,
Writer::class.java.name,
// TODO: add more from java.io.
// java.lang.invoke classes.
CallSite::class.java.name, // for all CallSites eg MutableCallSite, VolatileCallSite etc.
LambdaMetafactory::class.java.name,
MethodHandle::class.java.name,
MethodHandleProxies::class.java.name,
MethodHandles::class.java.name,
MethodHandles.Lookup::class.java.name,
MethodType::class.java.name,
SerializedLambda::class.java.name,
SwitchPoint::class.java.name,
// java.lang.invoke interfaces.
MethodHandleInfo::class.java.name,
// java.lang.invoke exceptions.
LambdaConversionException::class.java.name,
WrongMethodTypeException::class.java.name,
// java.lang.reflect.
AccessibleObject::class.java.name, // For Executable, Field, Method, Constructor.
Modifier::class.java.name,
Parameter::class.java.name,
ReflectPermission::class.java.name
// TODO: add more from java.lang.reflect.
)
// Specifically exclude classes from the blacklist,
// even if any of their superclasses and/or implemented interfaces are blacklisted.
private val forciblyAllowedClasses = hashSetOf<String>(
LinkedHashSet::class.java.name,
LinkedHashMap::class.java.name,
InputStream::class.java.name,
BufferedInputStream::class.java.name,
Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream").name
)
/**
* This implementation supports inheritance; thus, if a superclass or superinterface is blacklisted, so is the input class.
*/
override fun hasListed(type: Class<*>): Boolean {
// Check if excluded.
if (type.name !in forciblyAllowedClasses) {
// Check if listed.
if (type.name in blacklistedClasses)
throw IllegalStateException("Class ${type.name} is blacklisted, so it cannot be used in serialization.")
// Inheritance check.
else {
val aMatch = blacklistedClasses.firstOrNull { Class.forName(it).isAssignableFrom(type) }
if (aMatch != null) {
// TODO: blacklistedClasses += type.name // add it, so checking is faster next time we encounter this class.
val matchType = if (Class.forName(aMatch).isInterface) "superinterface" else "superclass"
throw IllegalStateException("The $matchType $aMatch of ${type.name} is blacklisted, so it cannot be used in serialization.")
}
}
}
return true
}
}

View File

@ -25,6 +25,10 @@ fun makeNoWhitelistClassResolver(): ClassResolver {
return CordaClassResolver(AllWhitelist)
}
fun makeAllButBlacklistedClassResolver(): ClassResolver {
return CordaClassResolver(AllButBlacklisted)
}
class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() {
/** Returns the registration for the specified class, or null if the class is not registered. */
override fun getRegistration(type: Class<*>): Registration? {
@ -55,8 +59,9 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
return checkClass(type.superclass)
}
// It's safe to have the Class already, since Kryo loads it with initialisation off.
val hasAnnotation = checkForAnnotation(type)
if (!hasAnnotation && !whitelist.hasListed(type)) {
// If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw a NotSerializableException if input class is blacklisted.
// Thus, blacklisting precedes annotation checking.
if (!whitelist.hasListed(type) && !checkForAnnotation(type)) {
throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization")
}
return null

View File

@ -18,6 +18,7 @@ import net.corda.core.utilities.NonEmptySetSerializer
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPrivateKey
import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey
@ -102,11 +103,8 @@ object DefaultKryoCustomizer {
register(CertPath::class.java, CertPathSerializer)
register(X509CertPath::class.java, CertPathSerializer)
// TODO: We shouldn't need to serialize raw certificates, and if we do then we need a cleaner solution
// than this mess.
val x509CertObjectClazz = Class.forName("org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject")
register(x509CertObjectClazz, X509CertificateSerializer)
register(X500Name::class.java, X500NameSerializer)
register(X509CertificateHolder::class.java, X509CertificateSerializer)
register(BCECPrivateKey::class.java, PrivateKeySerializer)
register(BCECPublicKey::class.java, PublicKeySerializer)

View File

@ -21,6 +21,7 @@ import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.cert.X509CertificateHolder
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.ByteArrayInputStream
@ -33,7 +34,6 @@ import java.security.PrivateKey
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.security.spec.InvalidKeySpecException
import java.time.Instant
import java.util.*
@ -329,7 +329,7 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
kryo.writeClassAndObject(output, obj.notary)
kryo.writeClassAndObject(output, obj.mustSign)
kryo.writeClassAndObject(output, obj.type)
kryo.writeClassAndObject(output, obj.timestamp)
kryo.writeClassAndObject(output, obj.timeWindow)
}
private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? {
@ -357,8 +357,8 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
val notary = kryo.readClassAndObject(input) as Party?
val signers = kryo.readClassAndObject(input) as List<PublicKey>
val transactionType = kryo.readClassAndObject(input) as TransactionType
val timestamp = kryo.readClassAndObject(input) as Timestamp?
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timestamp)
val timeWindow = kryo.readClassAndObject(input) as TimeWindow?
return WireTransaction(inputs, attachmentHashes, outputs, commands, notary, signers, transactionType, timeWindow)
}
}
}
@ -463,7 +463,7 @@ object KotlinObjectSerializer : Serializer<DeserializeAsKotlinObjectDef>() {
}
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeNoWhitelistClassResolver())) }.build()
private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build()
private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build()
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
@ -636,16 +636,15 @@ object CertPathSerializer : Serializer<CertPath>() {
}
/**
* For serialising an [CX509Certificate] in an X.500 standard format.
* For serialising an [CX509CertificateHolder] 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
object X509CertificateSerializer : Serializer<X509CertificateHolder>() {
override fun read(kryo: Kryo, input: Input, type: Class<X509CertificateHolder>): X509CertificateHolder {
return X509CertificateHolder(input.readBytes())
}
override fun write(kryo: Kryo, output: Output, obj: X509Certificate) {
override fun write(kryo: Kryo, output: Output, obj: X509CertificateHolder) {
output.writeBytes(obj.encoded)
}
}

View File

@ -7,7 +7,7 @@ import java.lang.reflect.Type
/**
* Serializer / deserializer for native AMQP types (Int, Float, String etc).
*/
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer {
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
override val typeDescriptor: String = SerializerFactory.primitiveTypeName(Primitives.wrap(clazz))!!
override val type: Type = clazz
@ -19,5 +19,5 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer {
data.putObject(obj)
}
override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any = obj
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = obj
}

View File

@ -6,7 +6,7 @@ import java.lang.reflect.Type
/**
* Implemented to serialize and deserialize different types of objects to/from AMQP.
*/
interface AMQPSerializer {
interface AMQPSerializer<out T> {
/**
* The JVM type this can serialize and deserialize.
*/
@ -34,5 +34,5 @@ interface AMQPSerializer {
/**
* Read the given object from the input. The envelope is provided in case the schema is required.
*/
fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any
fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T
}

View File

@ -9,14 +9,12 @@ import java.lang.reflect.Type
/**
* Serialization / deserialization of arrays.
*/
class ArraySerializer(override val type: Type) : AMQPSerializer {
private val typeName = type.typeName
class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
internal val elementType: Type = makeElementType()
private val elementType: Type = makeElementType()
private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
private val typeNotation: TypeNotation = RestrictedType(type.typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
private fun makeElementType(): Type {
return (type as? Class<*>)?.componentType ?: (type as GenericArrayType).genericComponentType
@ -39,8 +37,10 @@ class ArraySerializer(override val type: Type) : AMQPSerializer {
}
}
override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
return (obj as List<*>).map { input.readObjectOrNull(it, envelope, elementType) }.toArrayOfType(elementType)
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
if (obj is List<*>) {
return obj.map { input.readObjectOrNull(it, schema, elementType) }.toArrayOfType(elementType)
} else throw NotSerializableException("Expected a List but found $obj")
}
private fun <T> List<T>.toArrayOfType(type: Type): Any {

View File

@ -12,28 +12,27 @@ import kotlin.collections.Set
/**
* Serialization / deserialization of predefined set of supported [Collection] types covering mostly [List]s and [Set]s.
*/
class CollectionSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
class CollectionSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
private val typeName = declaredType.toString()
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
companion object {
private val supportedTypes: Map<Class<out Collection<*>>, (Collection<*>) -> Collection<*>> = mapOf(
Collection::class.java to { coll -> coll },
List::class.java to { coll -> coll },
Set::class.java to { coll -> Collections.unmodifiableSet(LinkedHashSet(coll)) },
SortedSet::class.java to { coll -> Collections.unmodifiableSortedSet(TreeSet(coll)) },
NavigableSet::class.java to { coll -> Collections.unmodifiableNavigableSet(TreeSet(coll)) }
private val supportedTypes: Map<Class<out Collection<*>>, (List<*>) -> Collection<*>> = mapOf(
Collection::class.java to { list -> Collections.unmodifiableCollection(list) },
List::class.java to { list -> Collections.unmodifiableList(list) },
Set::class.java to { list -> Collections.unmodifiableSet(LinkedHashSet(list)) },
SortedSet::class.java to { list -> Collections.unmodifiableSortedSet(TreeSet(list)) },
NavigableSet::class.java to { list -> Collections.unmodifiableNavigableSet(TreeSet(list)) }
)
private fun findConcreteType(clazz: Class<*>): (List<*>) -> Collection<*> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported collection type $clazz.")
}
}
private val concreteBuilder: (Collection<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
private fun findConcreteType(clazz: Class<*>): (Collection<*>) -> Collection<*> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
}
private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
@ -52,8 +51,8 @@ class CollectionSerializer(val declaredType: ParameterizedType) : AMQPSerializer
}
}
override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
// TODO: Can we verify the entries in the list?
return concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, envelope, declaredType.actualTypeArguments[0]) })
return concreteBuilder((obj as List<*>).map { input.readObjectOrNull(it, schema, declaredType.actualTypeArguments[0]) })
}
}

View File

@ -0,0 +1,105 @@
package net.corda.core.serialization.amqp
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
/**
* Base class for serializers of core platform types that do not conform to the usual serialization rules and thus
* cannot be automatically serialized.
*/
abstract class CustomSerializer<T> : AMQPSerializer<T> {
/**
* This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects
* that refer to arrays of types etc.
*/
abstract val additionalSerializers: Iterable<CustomSerializer<out Any>>
abstract fun isSerializerFor(clazz: Class<*>): Boolean
protected abstract val descriptor: Descriptor
/**
* This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization
* code path.
*/
abstract val schemaForDocumentation: Schema
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
data.withDescribed(descriptor) {
@Suppress("UNCHECKED_CAST")
writeDescribedObject(obj as T, data, type, output)
}
}
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput)
/**
* Additional base features for a custom serializer that is a particular class.
*/
abstract class Is<T>(protected val clazz: Class<T>) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
}
/**
* Additional base features for a custom serializer for all implementations of a particular interface or super class.
*/
abstract class Implements<T>(protected val clazz: Class<T>) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz)
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
}
/**
* Addition base features over and above [Implements] or [Is] custom serializer for when the serialize form should be
* the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization.
*
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
* custom serializers.
*/
abstract class Proxy<T, P>(protected val clazz: Class<T>,
protected val proxyClass: Class<P>,
protected val factory: SerializerFactory,
val withInheritance: Boolean = true) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
override val schemaForDocumentation: Schema by lazy {
val typeNotations = mutableSetOf<TypeNotation>(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields))
for (additional in additionalSerializers) {
typeNotations.addAll(additional.schemaForDocumentation.types)
}
Schema(typeNotations.toList())
}
/**
* Implement these two methods.
*/
protected abstract fun toProxy(obj: T): P
protected abstract fun fromProxy(proxy: P): T
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
val proxy = toProxy(obj)
data.withList {
for (property in proxySerializer.propertySerializers) {
property.writeProperty(proxy, this, output)
}
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
@Suppress("UNCHECKED_CAST")
val proxy = proxySerializer.readObject(obj, schema, input) as P
return fromProxy(proxy)
}
}
}

View File

@ -15,7 +15,7 @@ import java.util.*
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
* instances and threads.
*/
class DeserializationInput(private val serializerFactory: SerializerFactory = SerializerFactory()) {
class DeserializationInput(internal val serializerFactory: SerializerFactory = SerializerFactory()) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableList<Any> = ArrayList()
@ -41,7 +41,7 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se
}
val envelope = Envelope.get(data)
// Now pick out the obj and schema from the envelope.
return clazz.cast(readObjectOrNull(envelope.obj, envelope, clazz))
return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
} catch(nse: NotSerializableException) {
throw nse
} catch(t: Throwable) {
@ -51,20 +51,21 @@ class DeserializationInput(private val serializerFactory: SerializerFactory = Se
}
}
internal fun readObjectOrNull(obj: Any?, envelope: Envelope, type: Type): Any? {
internal fun readObjectOrNull(obj: Any?, schema: Schema, type: Type): Any? {
if (obj == null) {
return null
} else {
return readObject(obj, envelope, type)
return readObject(obj, schema, type)
}
}
internal fun readObject(obj: Any, envelope: Envelope, type: Type): Any {
internal fun readObject(obj: Any, schema: Schema, type: Type): Any {
if (obj is DescribedType) {
// Look up serializer in factory by descriptor
val serializer = serializerFactory.get(obj.descriptor, envelope)
if (serializer.type != type && !serializer.type.isSubClassOf(type)) throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
return serializer.readObject(obj.described, envelope, this)
val serializer = serializerFactory.get(obj.descriptor, schema)
if (serializer.type != type && !serializer.type.isSubClassOf(type))
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
return serializer.readObject(obj.described, schema, this)
} else {
return obj
}

View File

@ -119,7 +119,7 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
private fun makeType(typeName: String, cl: ClassLoader): Type {
// Not generic
return if (typeName == "*") SerializerFactory.AnyType else Class.forName(typeName, false, cl)
return if (typeName == "?") SerializerFactory.AnyType else Class.forName(typeName, false, cl)
}
private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type {

View File

@ -13,10 +13,9 @@ import kotlin.collections.map
/**
* Serialization / deserialization of certain supported [Map] types.
*/
class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type = declaredType as? DeserializedParameterizedType ?: DeserializedParameterizedType.make(declaredType.toString())
private val typeName = declaredType.toString()
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
companion object {
private val supportedTypes: Map<Class<out Map<*, *>>, (Map<*, *>) -> Map<*, *>> = mapOf(
@ -24,15 +23,15 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
SortedMap::class.java to { map -> Collections.unmodifiableSortedMap(TreeMap(map)) },
NavigableMap::class.java to { map -> Collections.unmodifiableNavigableMap(TreeMap(map)) }
)
private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
}
}
private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>)
private fun findConcreteType(clazz: Class<*>): (Map<*, *>) -> Map<*, *> {
return supportedTypes[clazz] ?: throw NotSerializableException("Unsupported map type $clazz.")
}
private val typeNotation: TypeNotation = RestrictedType(typeName, null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
@ -56,11 +55,13 @@ class MapSerializer(val declaredType: ParameterizedType) : AMQPSerializer {
}
}
override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
// TODO: General generics question. Do we need to validate that entries in Maps and Collections match the generic type? Is it a security hole?
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(envelope, input, it) }
val entries: Iterable<Pair<Any?, Any?>> = (obj as Map<*, *>).map { readEntry(schema, input, it) }
return concreteBuilder(entries.toMap())
}
private fun readEntry(envelope: Envelope, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) = input.readObjectOrNull(entry.key, envelope, declaredType.actualTypeArguments[0]) to input.readObjectOrNull(entry.value, envelope, declaredType.actualTypeArguments[1])
private fun readEntry(schema: Schema, input: DeserializationInput, entry: Map.Entry<Any?, Any?>) =
input.readObjectOrNull(entry.key, schema, declaredType.actualTypeArguments[0]) to
input.readObjectOrNull(entry.value, schema, declaredType.actualTypeArguments[1])
}

View File

@ -10,26 +10,30 @@ import kotlin.reflect.jvm.javaConstructor
/**
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor).
*/
class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer {
class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz
private val javaConstructor: Constructor<Any>?
private val propertySerializers: Collection<PropertySerializer>
internal val propertySerializers: Collection<PropertySerializer>
init {
val kotlinConstructor = constructorForDeserialization(clazz)
javaConstructor = kotlinConstructor?.javaConstructor
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz)
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
}
private val typeName = clazz.name
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type)}"
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict.
private val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
internal val typeNotation: TypeNotation = CompositeType(typeName, null, generateProvides(), Descriptor(typeDescriptor, null), generateFields())
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)
for (iface in interfaces) {
output.requireSerializer(iface)
if (output.writeTypeNotations(typeNotation)) {
for (iface in interfaces) {
output.requireSerializer(iface)
}
for (property in propertySerializers) {
property.writeClassInfo(output)
}
}
}
@ -45,13 +49,13 @@ class ObjectSerializer(val clazz: Class<*>) : AMQPSerializer {
}
}
override fun readObject(obj: Any, envelope: Envelope, input: DeserializationInput): Any {
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
if (obj is UnsignedInteger) {
// TODO: Object refs
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
} else if (obj is List<*>) {
if (obj.size > propertySerializers.size) throw NotSerializableException("Too many properties in described type $typeName")
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, envelope, input) }
val params = obj.zip(propertySerializers).map { it.second.readProperty(it.first, schema, input) }
return construct(params)
} else throw NotSerializableException("Body of described type is unexpected $obj")
}

View File

@ -9,8 +9,9 @@ import kotlin.reflect.jvm.javaGetter
* Base class for serialization of a property of an object.
*/
sealed class PropertySerializer(val name: String, val readMethod: Method) {
abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
abstract fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any?
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
val type: String = generateType()
val requires: List<String> = generateRequires()
@ -53,13 +54,13 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
}
companion object {
fun make(name: String, readMethod: Method): PropertySerializer {
fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer {
val type = readMethod.genericReturnType
if (SerializerFactory.isPrimitive(type)) {
// This is a little inefficient for performance since it does a runtime check of type. We could do build time check with lots of subclasses here.
return AMQPPrimitivePropertySerializer(name, readMethod)
} else {
return DescribedTypePropertySerializer(name, readMethod)
return DescribedTypePropertySerializer(name, readMethod) { factory.get(null, type) }
}
}
}
@ -67,9 +68,16 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
/**
* A property serializer for a complex type (another object).
*/
class DescribedTypePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) {
override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? {
return input.readObjectOrNull(obj, envelope, readMethod.genericReturnType)
class DescribedTypePropertySerializer(name: String, readMethod: Method, private val lazyTypeSerializer: () -> AMQPSerializer<Any>) : PropertySerializer(name, readMethod) {
// This is lazy so we don't get an infinite loop when a method returns an instance of the class.
private val typeSerializer: AMQPSerializer<Any> by lazy { lazyTypeSerializer() }
override fun writeClassInfo(output: SerializationOutput) {
typeSerializer.writeClassInfo(output)
}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
return input.readObjectOrNull(obj, schema, readMethod.genericReturnType)
}
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
@ -81,7 +89,9 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
* A property serializer for an AMQP primitive type (Int, String, etc).
*/
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) {
override fun readProperty(obj: Any?, envelope: Envelope, input: DeserializationInput): Any? {
override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
return obj
}

View File

@ -87,7 +87,7 @@ data class Schema(val types: List<TypeNotation>) : DescribedType {
override fun toString(): String = types.joinToString("\n")
}
data class Descriptor(val name: String?, val code: UnsignedLong?) : DescribedType {
data class Descriptor(val name: String?, val code: UnsignedLong? = null) : DescribedType {
companion object : DescribedTypeConstructor<Descriptor> {
val DESCRIPTOR = UnsignedLong(3L or DESCRIPTOR_TOP_32BITS)
@ -320,9 +320,9 @@ private val ANY_TYPE_HASH: String = "Any type = true"
* different.
*/
// TODO: write tests
internal fun fingerprintForType(type: Type): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher()).hash().asBytes())
internal fun fingerprintForType(type: Type, factory: SerializerFactory): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes())
private fun fingerprintForType(type: Type, alreadySeen: MutableSet<Type>, hasher: Hasher): Hasher {
private fun fingerprintForType(type: Type, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
return if (type in alreadySeen) {
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
} else {
@ -331,25 +331,31 @@ private fun fingerprintForType(type: Type, alreadySeen: MutableSet<Type>, hasher
hasher.putUnencodedChars(ANY_TYPE_HASH)
} else if (type is Class<*>) {
if (type.isArray) {
fingerprintForType(type.componentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH)
fingerprintForType(type.componentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) {
hasher.putUnencodedChars(type.name)
} else {
// Hash the class + properties + interfaces
propertiesForSerialization(constructorForDeserialization(type), type).fold(hasher.putUnencodedChars(type.name)) { orig, param ->
fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
// Need to check if a custom serializer is applicable
val customSerializer = factory.findCustomSerializer(type)
if (customSerializer == null) {
// Hash the class + properties + interfaces
propertiesForSerialization(constructorForDeserialization(type), type, factory).fold(hasher.putUnencodedChars(type.name)) { orig, param ->
fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig, factory).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
}
interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher, factory) }
hasher
} else {
hasher.putUnencodedChars(customSerializer.typeDescriptor)
}
interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher) }
hasher
}
} else if (type is ParameterizedType) {
// Hash the rawType + params
type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig) }
type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher, factory)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig, factory) }
} else if (type is GenericArrayType) {
// Hash the element type + some array hash
fingerprintForType(type.genericComponentType, alreadySeen, hasher).putUnencodedChars(ARRAY_HASH)
fingerprintForType(type.genericComponentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else {
throw NotSerializableException("Don't know how to hash $type")
}

View File

@ -1,14 +1,16 @@
package net.corda.core.serialization.amqp
import com.google.common.reflect.TypeToken
import org.apache.qpid.proton.codec.Data
import java.beans.Introspector
import java.beans.PropertyDescriptor
import java.io.NotSerializableException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaType
@ -58,24 +60,26 @@ internal fun <T : Any> constructorForDeserialization(clazz: Class<T>): KFunction
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have
* names accessible via reflection.
*/
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, clazz: Class<*>): Collection<PropertySerializer> {
return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor) else propertiesForSerialization(clazz)
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, clazz: Class<*>, factory: SerializerFactory): Collection<PropertySerializer> {
return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory)
}
private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>): Collection<PropertySerializer> {
private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>, factory: SerializerFactory): Collection<PropertySerializer> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties: Map<String, PropertyDescriptor> = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
val rc: MutableList<PropertySerializer> = ArrayList(kotlinConstructor.parameters.size)
for (param in kotlinConstructor.parameters) {
val name = param.name ?: throw NotSerializableException("Constructor parameter of $clazz has no name.")
val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz. If using Java, check that you have the -parameters option specified in the Java compiler.")
val matchingProperty = properties[name] ?: throw NotSerializableException("No property matching constructor parameter named $name of $clazz." +
" If using Java, check that you have the -parameters option specified in the Java compiler.")
// Check that the method has a getter in java.
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz. If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
if (getter.genericReturnType == param.type.javaType) {
rc += PropertySerializer.make(name, getter)
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." +
" If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
if (constructorParamTakesReturnTypeOfGetter(getter, param)) {
rc += PropertySerializer.make(name, getter, factory)
} else {
throw NotSerializableException("Property type ${getter.genericReturnType} for $name of $clazz differs from constructor parameter type ${param.type.javaType}")
}
@ -83,14 +87,16 @@ private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>
return rc
}
private fun propertiesForSerialization(clazz: Class<*>): Collection<PropertySerializer> {
private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType)
private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection<PropertySerializer> {
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }
val rc: MutableList<PropertySerializer> = ArrayList(properties.size)
for (property in properties) {
// Check that the method has a getter in java.
val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.")
rc += PropertySerializer.make(property.name, getter)
rc += PropertySerializer.make(property.name, getter, factory)
}
return rc
}
@ -104,6 +110,7 @@ internal fun interfacesForSerialization(clazz: Class<*>): List<Type> {
private fun exploreType(type: Type?, interfaces: MutableSet<Type>) {
val clazz = (type as? Class<*>) ?: (type as? ParameterizedType)?.rawType as? Class<*>
if (clazz != null) {
if (clazz.isInterface) interfaces += clazz
for (newInterface in clazz.genericInterfaces) {
if (newInterface !in interfaces) {
interfaces += newInterface

View File

@ -14,10 +14,10 @@ import kotlin.collections.LinkedHashSet
* @param serializerFactory This is the factory for [AMQPSerializer] instances and can be shared across multiple
* instances and threads.
*/
class SerializationOutput(private val serializerFactory: SerializerFactory = SerializerFactory()) {
open class SerializationOutput(internal val serializerFactory: SerializerFactory = SerializerFactory()) {
// TODO: we're not supporting object refs yet
private val objectHistory: MutableMap<Any, Int> = IdentityHashMap()
private val serializerHistory: MutableSet<AMQPSerializer> = LinkedHashSet()
private val serializerHistory: MutableSet<AMQPSerializer<*>> = LinkedHashSet()
private val schemaHistory: MutableSet<TypeNotation> = LinkedHashSet()
/**
@ -64,19 +64,21 @@ class SerializationOutput(private val serializerFactory: SerializerFactory = Ser
internal fun writeObject(obj: Any, data: Data, type: Type) {
val serializer = serializerFactory.get(obj.javaClass, type)
if (serializer !in serializerHistory) {
serializerHistory.add(serializer)
serializer.writeClassInfo(this)
}
serializer.writeObject(obj, data, type, this)
}
internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {
open internal fun writeTypeNotations(vararg typeNotation: TypeNotation): Boolean {
return schemaHistory.addAll(typeNotation)
}
internal fun requireSerializer(type: Type) {
if (type != SerializerFactory.AnyType) {
open internal fun requireSerializer(type: Type) {
if (type != SerializerFactory.AnyType && type != Object::class.java) {
val serializer = serializerFactory.get(null, type)
if (serializer !in serializerHistory) {
serializerHistory.add(serializer)
serializer.writeClassInfo(this)
}
}

View File

@ -10,18 +10,19 @@ import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.WildcardType
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.CopyOnWriteArrayList
import javax.annotation.concurrent.ThreadSafe
/**
* Factory of serializers designed to be shared across threads and invocations.
*/
// TODO: enums
// TODO: object references
// TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal)
// TODO: Inner classes etc
// TODO: support for custom serialisation of core types (of e.g. PublicKey, Throwables)
// TODO: exclude schemas for core types that don't need custom serializers that everyone already knows the schema for.
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
// TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general
@ -30,10 +31,13 @@ import javax.annotation.concurrent.ThreadSafe
// TODO: incorporate the class carpenter for classes not on the classpath.
// TODO: apply class loader logic and an "app context" throughout this code.
// TODO: schema evolution solution when the fingerprints do not line up.
// TODO: allow definition of well known types that are left out of the schema.
// TODO: automatically support byte[] without having to wrap in [Binary].
@ThreadSafe
class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer>()
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer>()
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
private val serializersByDescriptor = ConcurrentHashMap<Any, AMQPSerializer<Any>>()
private val customSerializers = CopyOnWriteArrayList<CustomSerializer<out Any>>()
/**
* Look up, and manufacture if necessary, a serializer for the given type.
@ -42,7 +46,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
* restricted type processing).
*/
@Throws(NotSerializableException::class)
fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer {
fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
if (declaredType is ParameterizedType) {
return serializersByType.computeIfAbsent(declaredType) {
// We allow only Collection and Map.
@ -50,7 +54,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
if (rawType is Class<*>) {
checkParameterisedTypesConcrete(declaredType.actualTypeArguments)
if (Collection::class.java.isAssignableFrom(rawType)) {
CollectionSerializer(declaredType)
CollectionSerializer(declaredType, this)
} else if (Map::class.java.isAssignableFrom(rawType)) {
makeMapSerializer(declaredType)
} else {
@ -63,27 +67,44 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
} else if (declaredType is Class<*>) {
// Simple classes allowed
if (Collection::class.java.isAssignableFrom(declaredType)) {
return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null)) }
return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null), this) }
} else if (Map::class.java.isAssignableFrom(declaredType)) {
return serializersByType.computeIfAbsent(declaredType) { makeMapSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType, AnyType), null)) }
} else {
return makeClassSerializer(actualType ?: declaredType)
}
} else if (declaredType is GenericArrayType) {
return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType) }
return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) }
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
}
}
/**
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
* contained in the [Schema].
*/
@Throws(NotSerializableException::class)
fun get(typeDescriptor: Any, envelope: Envelope): AMQPSerializer {
fun get(typeDescriptor: Any, schema: Schema): AMQPSerializer<Any> {
return serializersByDescriptor[typeDescriptor] ?: {
processSchema(envelope.schema)
processSchema(schema)
serializersByDescriptor[typeDescriptor] ?: throw NotSerializableException("Could not find type matching descriptor $typeDescriptor.")
}()
}
/**
* TODO: Add docs
*/
fun register(customSerializer: CustomSerializer<out Any>) {
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
customSerializers += customSerializer
serializersByDescriptor[customSerializer.typeDescriptor] = customSerializer
for (additional in customSerializer.additionalSerializers) {
register(additional)
}
}
}
private fun processSchema(schema: Schema) {
for (typeNotation in schema.types) {
processSchemaEntry(typeNotation)
@ -99,7 +120,14 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private fun restrictedTypeForName(name: String): Type {
return if (name.endsWith("[]")) {
DeserializedGenericArrayType(restrictedTypeForName(name.substring(0, name.lastIndex - 1)))
val elementType = restrictedTypeForName(name.substring(0, name.lastIndex - 1))
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw NotSerializableException("Not able to deserialize array type: $name")
}
} else {
DeserializedParameterizedType.make(name)
}
@ -134,32 +162,52 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
}
}
private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer {
private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer<Any> {
return serializersByType.computeIfAbsent(clazz) {
if (clazz.isArray) {
whitelisted(clazz.componentType)
ArraySerializer(clazz)
} else if (isPrimitive(clazz)) {
if (isPrimitive(clazz)) {
AMQPPrimitiveSerializer(clazz)
} else {
whitelisted(clazz)
ObjectSerializer(clazz)
findCustomSerializer(clazz) ?: {
if (clazz.isArray) {
whitelisted(clazz.componentType)
ArraySerializer(clazz, this)
} else {
whitelisted(clazz)
ObjectSerializer(clazz, this)
}
}()
}
}
}
internal fun findCustomSerializer(clazz: Class<*>): AMQPSerializer<Any>? {
for (customSerializer in customSerializers) {
if (customSerializer.isSerializerFor(clazz)) {
return customSerializer
}
}
return null
}
private fun whitelisted(clazz: Class<*>): Boolean {
if (whitelist.hasListed(clazz) || clazz.isAnnotationPresent(CordaSerializable::class.java)) {
if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) {
return true
} else {
throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.")
}
}
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer {
// Recursively check the class, interfaces and superclasses for our annotation.
internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java) ||
type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationInHierarchy(it) }
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
}
private fun makeMapSerializer(declaredType: ParameterizedType): AMQPSerializer<Any> {
val rawType = declaredType.rawType as Class<*>
rawType.checkNotUnorderedHashMap()
return MapSerializer(declaredType)
return MapSerializer(declaredType, this)
}
companion object {
@ -185,12 +233,17 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
Char::class.java to "char",
Date::class.java to "timestamp",
UUID::class.java to "uuid",
ByteArray::class.java to "binary",
Binary::class.java to "binary",
String::class.java to "string",
Symbol::class.java to "symbol")
}
object AnyType : Type {
override fun toString(): String = "*"
object AnyType : WildcardType {
override fun getUpperBounds(): Array<Type> = arrayOf(Object::class.java)
override fun getLowerBounds(): Array<Type> = emptyArray()
override fun toString(): String = "?"
}
}

View File

@ -0,0 +1,24 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.crypto.Crypto
import net.corda.core.serialization.amqp.*
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.PublicKey
class PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::class.java) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(Binary::class.java)!!, descriptor, emptyList())))
override fun writeDescribedObject(obj: PublicKey, data: Data, type: Type, output: SerializationOutput) {
// TODO: Instead of encoding to the default X509 format, we could have a custom per key type (space-efficient) serialiser.
output.writeObject(Binary(obj.encoded), data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): PublicKey {
val A = input.readObject(obj, schema, ByteArray::class.java) as Binary
return Crypto.decodePublicKey(A.array)
}
}

View File

@ -0,0 +1,81 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.serialization.amqp.CustomSerializer
import net.corda.core.serialization.amqp.SerializerFactory
import net.corda.core.serialization.amqp.constructorForDeserialization
import net.corda.core.serialization.amqp.propertiesForSerialization
import net.corda.core.utilities.CordaRuntimeException
import net.corda.core.utilities.CordaThrowable
import java.io.NotSerializableException
class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = listOf(StackTraceElementSerializer(factory))
override fun toProxy(obj: Throwable): ThrowableProxy {
val extraProperties: MutableMap<String, Any?> = LinkedHashMap()
val message = if (obj is CordaThrowable) {
// Try and find a constructor
try {
val constructor = constructorForDeserialization(obj.javaClass)
val props = propertiesForSerialization(constructor, obj.javaClass, factory)
for (prop in props) {
extraProperties[prop.name] = prop.readMethod.invoke(obj)
}
} catch(e: NotSerializableException) {
}
obj.originalMessage
} else {
obj.message
}
return ThrowableProxy(obj.javaClass.name, message, obj.stackTrace, obj.cause, obj.suppressed, extraProperties)
}
override fun fromProxy(proxy: ThrowableProxy): Throwable {
try {
// TODO: This will need reworking when we have multiple class loaders
val clazz = Class.forName(proxy.exceptionClass, false, this.javaClass.classLoader)
// If it is CordaException or CordaRuntimeException, we can seek any constructor and then set the properties
// Otherwise we just make a CordaRuntimeException
if (CordaThrowable::class.java.isAssignableFrom(clazz) && Throwable::class.java.isAssignableFrom(clazz)) {
val constructor = constructorForDeserialization(clazz)!!
val throwable = constructor.callBy(constructor.parameters.map { it to proxy.additionalProperties[it.name] }.toMap())
(throwable as CordaThrowable).apply {
if (this.javaClass.name != proxy.exceptionClass) this.originalExceptionClassName = proxy.exceptionClass
this.setMessage(proxy.message)
this.setCause(proxy.cause)
this.addSuppressed(proxy.suppressed)
}
return (throwable as Throwable).apply {
this.stackTrace = proxy.stackTrace
}
}
} catch (e: Exception) {
// If attempts to rebuild the exact exception fail, we fall through and build a runtime exception.
}
// If the criteria are not met or we experience an exception constructing the exception, we fall back to our own unchecked exception.
return CordaRuntimeException(proxy.exceptionClass).apply {
this.setMessage(proxy.message)
this.setCause(proxy.cause)
this.stackTrace = proxy.stackTrace
this.addSuppressed(proxy.suppressed)
}
}
class ThrowableProxy(
val exceptionClass: String,
val message: String?,
val stackTrace: Array<StackTraceElement>,
val cause: Throwable?,
val suppressed: Array<Throwable>,
val additionalProperties: Map<String, Any?>)
}
class StackTraceElementSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<StackTraceElement, StackTraceElementSerializer.StackTraceElementProxy>(StackTraceElement::class.java, StackTraceElementProxy::class.java, factory) {
override val additionalSerializers: Iterable<CustomSerializer<Any>> = emptyList()
override fun toProxy(obj: StackTraceElement): StackTraceElementProxy = StackTraceElementProxy(obj.className, obj.methodName, obj.fileName, obj.lineNumber)
override fun fromProxy(proxy: StackTraceElementProxy): StackTraceElement = StackTraceElement(proxy.declaringClass, proxy.methodName, proxy.fileName, proxy.lineNumber)
data class StackTraceElementProxy(val declaringClass: String, val methodName: String, val fileName: String?, val lineNumber: Int)
}

View File

@ -36,12 +36,12 @@ abstract class BaseTransaction(
* If specified, a time window in which this transaction may have been notarised. Contracts can check this
* time window to find out when a transaction is deemed to have occurred, from the ledger's perspective.
*/
val timestamp: Timestamp?
val timeWindow: TimeWindow?
) : NamedByHash {
protected fun checkInvariants() {
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs." }
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
if (notary == null) check(inputs.isEmpty()) { "The notary must be specified explicitly for any transaction that has inputs" }
if (timeWindow != null) check(notary != null) { "If a time-window is provided, there must be a notary" }
}
override fun equals(other: Any?): Boolean {
@ -50,10 +50,10 @@ abstract class BaseTransaction(
notary == other.notary &&
mustSign == other.mustSign &&
type == other.type &&
timestamp == other.timestamp
timeWindow == other.timeWindow
}
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
override fun hashCode() = Objects.hash(notary, mustSign, type, timeWindow)
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
}

View File

@ -32,9 +32,9 @@ class LedgerTransaction(
override val id: SecureHash,
notary: Party?,
signers: List<PublicKey>,
timestamp: Timestamp?,
timeWindow: TimeWindow?,
type: TransactionType
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp) {
) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow) {
init {
checkInvariants()
}
@ -47,7 +47,7 @@ class LedgerTransaction(
/** Strips the transaction down to a form that is usable by the contract verify functions */
fun toTransactionForContract(): TransactionForContract {
return TransactionForContract(inputs.map { it.state.data }, outputs.map { it.data }, attachments, commands, id,
inputs.map { it.state.notary }.singleOrNull(), timestamp)
inputs.map { it.state.notary }.singleOrNull(), timeWindow)
}
/**

View File

@ -11,6 +11,7 @@ import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize
import net.corda.core.serialization.withoutReferences
import java.security.PublicKey
import java.util.function.Predicate
fun <T : Any> serializedHash(x: T): SecureHash {
return p2PKryo().run { kryo -> kryo.withoutReferences { x.serialize(kryo).hash } }
@ -33,7 +34,7 @@ interface TraversableTransaction {
val notary: Party?
val mustSign: List<PublicKey>
val type: TransactionType?
val timestamp: Timestamp?
val timeWindow: TimeWindow?
/**
* Returns a flattened list of all the components that are present in the transaction, in the following order:
@ -45,7 +46,7 @@ interface TraversableTransaction {
* - The notary [Party], if present
* - Each required signer ([mustSign]) that is present
* - The type of the transaction, if present
* - The timestamp of the transaction, if present
* - The time-window of the transaction, if present
*/
val availableComponents: List<Any>
get() {
@ -56,7 +57,7 @@ interface TraversableTransaction {
notary?.let { result += it }
result.addAll(mustSign)
type?.let { result += it }
timestamp?.let { result += it }
timeWindow?.let { result += it }
return result
}
@ -81,7 +82,7 @@ class FilteredLeaves(
override val notary: Party?,
override val mustSign: List<PublicKey>,
override val type: TransactionType?,
override val timestamp: Timestamp?
override val timeWindow: TimeWindow?
) : TraversableTransaction {
/**
* Function that checks the whole filtered structure.
@ -116,8 +117,9 @@ class FilteredTransaction private constructor(
* @param wtx WireTransaction to be filtered.
* @param filtering filtering over the whole WireTransaction
*/
@JvmStatic
fun buildMerkleTransaction(wtx: WireTransaction,
filtering: (Any) -> Boolean
filtering: Predicate<Any>
): FilteredTransaction {
val filteredLeaves = wtx.filterWithFun(filtering)
val merkleTree = wtx.merkleTree

View File

@ -26,9 +26,11 @@ import java.util.*
* when verifying composite key signatures, but may be used as individual signatures where a single key is expected to
* sign.
*/
// DOCSTART 1
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
val sigs: List<DigitalSignature.WithKey>
) : NamedByHash {
// DOCEND 1
init {
require(sigs.isNotEmpty())
}
@ -64,8 +66,10 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
* @throws SignatureException if any signatures are invalid or unrecognised.
* @throws SignaturesMissingException if any signatures should have been present but were not.
*/
// DOCSTART 2
@Throws(SignatureException::class)
fun verifySignatures(vararg allowedToBeMissing: PublicKey): WireTransaction {
// DOCEND 2
// Embedded WireTransaction is not deserialised until after we check the signatures.
checkSignaturesAreValid()

View File

@ -3,7 +3,7 @@ package net.corda.core.transactions
import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.flows.FlowStateMachine
import net.corda.core.internal.FlowStateMachine
import net.corda.core.identity.Party
import net.corda.core.serialization.serialize
import java.security.KeyPair
@ -37,9 +37,10 @@ open class TransactionBuilder(
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command> = arrayListOf(),
protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected var timestamp: Timestamp? = null) {
protected var timeWindow: TimeWindow? = null) {
constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
val time: Timestamp? get() = timestamp
val time: TimeWindow? get() = timeWindow // TODO: rename using a more descriptive name (i.e. timeWindowGetter) or remove if unused.
/**
* Creates a copy of the builder.
@ -53,30 +54,31 @@ open class TransactionBuilder(
outputs = ArrayList(outputs),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
timestamp = timestamp
timeWindow = timeWindow
)
/**
* Places a [TimestampCommand] in this transaction, removing any existing command if there is one.
* Places a [TimeWindow] in this transaction, removing any existing command if there is one.
* The command requires a signature from the Notary service, which acts as a Timestamp Authority.
* The signature can be obtained using [NotaryFlow].
*
* The window of time in which the final timestamp may lie is defined as [time] +/- [timeTolerance].
* The window of time in which the final time-window may lie is defined as [time] +/- [timeTolerance].
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
* window of time, taking into account factors such as network latency. Transactions being built by a group of
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node.
*/
fun setTime(time: Instant, timeTolerance: Duration) = setTime(Timestamp(time, timeTolerance))
fun addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance))
fun setTime(newTimestamp: Timestamp) {
check(notary != null) { "Only notarised transactions can have a timestamp" }
fun addTimeWindow(timeWindow: TimeWindow) {
check(notary != null) { "Only notarised transactions can have a time-window" }
signers.add(notary!!.owningKey)
check(currentSigs.isEmpty()) { "Cannot change timestamp after signing" }
this.timestamp = newTimestamp
check(currentSigs.isEmpty()) { "Cannot change time-window after signing" }
this.timeWindow = timeWindow
}
// DOCSTART 1
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
fun withItems(vararg items: Any): TransactionBuilder {
for (t in items) {
@ -91,10 +93,12 @@ open class TransactionBuilder(
}
return this
}
// DOCEND 1
/** The signatures that have been collected so far - might be incomplete! */
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
@Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun signWith(key: KeyPair): TransactionBuilder {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
val data = toWireTransaction().id
@ -132,7 +136,7 @@ open class TransactionBuilder(
}
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timestamp)
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow)
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {

View File

@ -13,6 +13,7 @@ import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize
import net.corda.core.utilities.Emoji
import java.security.PublicKey
import java.util.function.Predicate
/**
* A transaction ready for serialisation, without any signatures attached. A WireTransaction is usually wrapped
@ -31,8 +32,8 @@ class WireTransaction(
notary: Party?,
signers: List<PublicKey>,
type: TransactionType,
timestamp: Timestamp?
) : BaseTransaction(inputs, outputs, notary, signers, type, timestamp), TraversableTransaction {
timeWindow: TimeWindow?
) : BaseTransaction(inputs, outputs, notary, signers, type, timeWindow), TraversableTransaction {
init {
checkInvariants()
}
@ -100,13 +101,13 @@ class WireTransaction(
val resolvedInputs = inputs.map { ref ->
resolveStateRef(ref)?.let { StateAndRef(it, ref) } ?: throw TransactionResolutionException(ref.txhash)
}
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timestamp, type)
return LedgerTransaction(resolvedInputs, outputs, authenticatedArgs, attachments, id, notary, mustSign, timeWindow, type)
}
/**
* Build filtered transaction using provided filtering functions.
*/
fun buildFilteredTransaction(filtering: (Any) -> Boolean): FilteredTransaction {
fun buildFilteredTransaction(filtering: Predicate<Any>): FilteredTransaction {
return FilteredTransaction.buildMerkleTransaction(this, filtering)
}
@ -120,17 +121,17 @@ class WireTransaction(
* @param filtering filtering over the whole WireTransaction
* @returns FilteredLeaves used in PartialMerkleTree calculation and verification.
*/
fun filterWithFun(filtering: (Any) -> Boolean): FilteredLeaves {
fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering(elem)) null else elem
fun filterWithFun(filtering: Predicate<Any>): FilteredLeaves {
fun notNullFalse(elem: Any?): Any? = if (elem == null || !filtering.test(elem)) null else elem
return FilteredLeaves(
inputs.filter { filtering(it) },
attachments.filter { filtering(it) },
outputs.filter { filtering(it) },
commands.filter { filtering(it) },
inputs.filter { filtering.test(it) },
attachments.filter { filtering.test(it) },
outputs.filter { filtering.test(it) },
commands.filter { filtering.test(it) },
notNullFalse(notary) as Party?,
mustSign.filter { filtering(it) },
mustSign.filter { filtering.test(it) },
notNullFalse(type) as TransactionType?,
notNullFalse(timestamp) as Timestamp?
notNullFalse(timeWindow) as TimeWindow?
)
}

View File

@ -0,0 +1,103 @@
package net.corda.core.utilities
import net.corda.core.serialization.CordaSerializable
import java.util.*
@CordaSerializable
interface CordaThrowable {
var originalExceptionClassName: String?
val originalMessage: String?
fun setMessage(message: String?)
fun setCause(cause: Throwable?)
fun addSuppressed(suppressed: Array<Throwable>)
}
open class CordaException internal constructor(override var originalExceptionClassName: String? = null,
private var _message: String? = null,
private var _cause: Throwable? = null) : Exception(null, null, true, true), CordaThrowable {
constructor(message: String?,
cause: Throwable?) : this(null, message, cause)
override val message: String?
get() = if (originalExceptionClassName == null) originalMessage else {
if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
}
override val cause: Throwable?
get() = _cause ?: super.cause
override fun setMessage(message: String?) {
_message = message
}
override fun setCause(cause: Throwable?) {
_cause = cause
}
override fun addSuppressed(suppressed: Array<Throwable>) {
for (suppress in suppressed) {
addSuppressed(suppress)
}
}
override val originalMessage: String?
get() = _message
override fun hashCode(): Int {
return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage)
}
override fun equals(other: Any?): Boolean {
return other is CordaException &&
originalExceptionClassName == other.originalExceptionClassName &&
message == other.message &&
cause == other.cause &&
Arrays.equals(stackTrace, other.stackTrace) &&
Arrays.equals(suppressed, other.suppressed)
}
}
open class CordaRuntimeException internal constructor(override var originalExceptionClassName: String?,
private var _message: String? = null,
private var _cause: Throwable? = null) : RuntimeException(null, null, true, true), CordaThrowable {
constructor(message: String?, cause: Throwable?) : this(null, message, cause)
override val message: String?
get() = if (originalExceptionClassName == null) originalMessage else {
if (originalMessage == null) "$originalExceptionClassName" else "$originalExceptionClassName: $originalMessage"
}
override val cause: Throwable?
get() = _cause ?: super.cause
override fun setMessage(message: String?) {
_message = message
}
override fun setCause(cause: Throwable?) {
_cause = cause
}
override fun addSuppressed(suppressed: Array<Throwable>) {
for (suppress in suppressed) {
addSuppressed(suppress)
}
}
override val originalMessage: String?
get() = _message
override fun hashCode(): Int {
return Arrays.deepHashCode(stackTrace) xor Objects.hash(originalExceptionClassName, originalMessage)
}
override fun equals(other: Any?): Boolean {
return other is CordaRuntimeException &&
originalExceptionClassName == other.originalExceptionClassName &&
message == other.message &&
cause == other.cause &&
Arrays.equals(stackTrace, other.stackTrace) &&
Arrays.equals(suppressed, other.suppressed)
}
}

View File

@ -7,8 +7,12 @@ import net.corda.core.codePointsString
*/
object Emoji {
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
// However the JediTerm java terminal emulator can also do emoji on OS X when using the JetBrains JRE.
// Check for that here. DemoBench sets TERM_PROGRAM appropriately.
val hasEmojiTerminal by lazy {
System.getenv("CORDA_FORCE_EMOJI") != null || System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app")
System.getenv("CORDA_FORCE_EMOJI") != null ||
System.getenv("TERM_PROGRAM") in listOf("Apple_Terminal", "iTerm.app") ||
(System.getenv("TERM_PROGRAM") == "JediTerm" && System.getProperty("java.vendor") == "JetBrains s.r.o")
}
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)

View File

@ -2,21 +2,24 @@ package net.corda.core.utilities
import java.nio.file.Path
// TODO This doesn't belong in core and can be moved into node
object ProcessUtilities {
inline fun <reified C : Any> startJavaProcess(
arguments: List<String>,
classpath: String = defaultClassPath,
jdwpPort: Int? = null,
extraJvmArguments: List<String> = emptyList(),
inheritIO: Boolean = true,
errorLogPath: Path? = null,
workingDirectory: Path? = null
): Process {
return startJavaProcess(C::class.java.name, arguments, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
return startJavaProcess(C::class.java.name, arguments, classpath, jdwpPort, extraJvmArguments, inheritIO, errorLogPath, workingDirectory)
}
fun startJavaProcess(
className: String,
arguments: List<String>,
classpath: String = defaultClassPath,
jdwpPort: Int? = null,
extraJvmArguments: List<String> = emptyList(),
inheritIO: Boolean = true,
@ -24,7 +27,6 @@ object ProcessUtilities {
workingDirectory: Path? = null
): Process {
val separator = System.getProperty("file.separator")
val classpath = System.getProperty("java.class.path")
val javaPath = System.getProperty("java.home") + separator + "bin" + separator + "java"
val debugPortArgument = if (jdwpPort != null) {
listOf("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=$jdwpPort")
@ -44,4 +46,6 @@ object ProcessUtilities {
if (workingDirectory != null) directory(workingDirectory.toFile())
}.start()
}
val defaultClassPath: String get() = System.getProperty("java.class.path")
}

View File

@ -4,10 +4,12 @@ package net.corda.core.utilities
import net.corda.core.crypto.*
import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate
import org.bouncycastle.asn1.x500.X500Name
import java.math.BigInteger
import java.security.KeyPair
import java.security.PublicKey
import java.security.cert.CertificateFactory
import java.time.Instant
// A dummy time at which we will be pretending test transactions are created.
@ -21,6 +23,7 @@ val DUMMY_KEY_2: KeyPair by lazy { generateKeyPair() }
val DUMMY_NOTARY_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(20)) }
/** Dummy notary identity for tests and simulations */
val DUMMY_NOTARY_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(DUMMY_NOTARY)
val DUMMY_NOTARY: Party get() = Party(X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"), DUMMY_NOTARY_KEY.public)
val DUMMY_MAP_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(30)) }
@ -29,7 +32,7 @@ val DUMMY_MAP: Party get() = Party(X500Name("CN=Network Map Service,O=R3,OU=cord
val DUMMY_BANK_A_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(40)) }
/** Dummy bank identity for tests and simulations */
val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=UK"), DUMMY_BANK_A_KEY.public)
val DUMMY_BANK_A: Party get() = Party(X500Name("CN=Bank A,O=Bank A,L=London,C=GB"), DUMMY_BANK_A_KEY.public)
val DUMMY_BANK_B_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(50)) }
/** Dummy bank identity for tests and simulations */
@ -41,16 +44,37 @@ val DUMMY_BANK_C: Party get() = Party(X500Name("CN=Bank C,O=Bank C,L=Tokyo,C=JP"
val ALICE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(70)) }
/** Dummy individual identity for tests and simulations */
val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=London,C=UK"), ALICE_KEY.public)
val ALICE_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(ALICE)
val ALICE: Party get() = Party(X500Name("CN=Alice Corp,O=Alice Corp,L=Madrid,C=ES"), ALICE_KEY.public)
val BOB_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(80)) }
/** Dummy individual identity for tests and simulations */
val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=London,C=UK"), BOB_KEY.public)
val BOB_IDENTITY: PartyAndCertificate get() = getTestPartyAndCertificate(BOB)
val BOB: Party get() = Party(X500Name("CN=Bob Plc,O=Bob Plc,L=Rome,C=IT"), BOB_KEY.public)
val CHARLIE_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(90)) }
/** Dummy individual identity for tests and simulations */
val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=London,C=UK"), CHARLIE_KEY.public)
val CHARLIE: Party get() = Party(X500Name("CN=Charlie Ltd,O=Charlie Ltd,L=Athens,C=GR"), CHARLIE_KEY.public)
val DUMMY_REGULATOR_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(100)) }
/** Dummy regulator for tests and simulations */
val DUMMY_REGULATOR: Party get() = Party(X500Name("CN=Regulator A,OU=Corda,O=AMF,L=Paris,C=FR"), DUMMY_REGULATOR_KEY.public)
val DUMMY_CA_KEY: KeyPair by lazy { entropyToKeyPair(BigInteger.valueOf(110)) }
val DUMMY_CA: CertificateAndKeyPair by lazy {
// TODO: Should be identity scheme
val cert = X509Utilities.createSelfSignedCACertificate(X500Name("CN=Dummy CA,OU=Corda,O=R3 Ltd,L=London,C=GB"), DUMMY_CA_KEY)
CertificateAndKeyPair(cert, DUMMY_CA_KEY)
}
/**
* Build a test party with a nonsense certificate authority for testing purposes.
*/
fun getTestPartyAndCertificate(name: X500Name, publicKey: PublicKey, trustRoot: CertificateAndKeyPair = DUMMY_CA) = getTestPartyAndCertificate(Party(name, publicKey), trustRoot)
fun getTestPartyAndCertificate(party: Party, trustRoot: CertificateAndKeyPair = DUMMY_CA): PartyAndCertificate {
val certFactory = CertificateFactory.getInstance("X509")
val certHolder = X509Utilities.createCertificate(CertificateType.IDENTITY, trustRoot.certificate, trustRoot.keyPair, party.name, party.owningKey)
val certPath = certFactory.generateCertPath(listOf(certHolder.cert, trustRoot.certificate.cert))
return PartyAndCertificate(party, certHolder, certPath)
}

View File

@ -1,12 +0,0 @@
package net.corda.core.utilities
import java.time.Duration
import java.time.Instant
/**
* A class representing a window in time from a particular instant, lasting a specified duration.
*/
data class TimeWindow(val start: Instant, val duration: Duration) {
val end: Instant
get() = start + duration
}

View File

@ -60,7 +60,6 @@ import java.security.PublicKey
* @param partiallySignedTx Transaction to collect the remaining signatures for
*/
// TODO: AbstractStateReplacementFlow needs updating to use this flow.
// TODO: TwoPartyTradeFlow needs updating to use this flow.
// TODO: Update this flow to handle randomly generated keys when that works is complete.
class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
override val progressTracker: ProgressTracker = tracker()): FlowLogic<SignedTransaction>() {
@ -123,6 +122,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
partyNode.legalIdentity
}
// DOCSTART 1
/**
* Get and check the required signature.
*/
@ -132,6 +132,7 @@ class CollectSignaturesFlow(val partiallySignedTx: SignedTransaction,
it
}
}
// DOCEND 1
}
/**

View File

@ -89,7 +89,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
private fun needsNotarySignature(stx: SignedTransaction): Boolean {
val wtx = stx.tx
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timestamp != null
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timeWindow != null
return needsNotarisation && hasNoNotarySignature(stx)
}

View File

@ -7,7 +7,6 @@ import net.corda.core.identity.Party
import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.utilities.ProgressTracker
import java.security.PublicKey
/**
* A flow to be used for changing a state's Notary. This is required since all input states to a transaction

View File

@ -2,7 +2,7 @@ package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.Timestamp
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
@ -11,7 +11,7 @@ import net.corda.core.flows.FlowException
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatingFlow
import net.corda.core.identity.Party
import net.corda.core.node.services.TimestampChecker
import net.corda.core.node.services.TimeWindowChecker
import net.corda.core.node.services.UniquenessException
import net.corda.core.node.services.UniquenessProvider
import net.corda.core.serialization.CordaSerializable
@ -19,17 +19,18 @@ import net.corda.core.serialization.serialize
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import java.util.function.Predicate
object NotaryFlow {
/**
* A flow to be used by a party for obtaining signature(s) from a [NotaryService] ascertaining the transaction
* timestamp is correct and none of its inputs have been used in another completed transaction.
* time-window is correct and none of its inputs have been used in another completed transaction.
*
* In case of a single-node or Raft notary, the flow will return a single signature. For the BFT notary multiple
* signatures will be returned one from each replica that accepted the input state commit.
*
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
* by another transaction or the timestamp is invalid.
* by another transaction or the time-window is invalid.
*/
@InitiatingFlow
open class Client(private val stx: SignedTransaction,
@ -63,7 +64,7 @@ object NotaryFlow {
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
stx
} else {
wtx.buildFilteredTransaction { it is StateRef || it is Timestamp }
wtx.buildFilteredTransaction(Predicate { it is StateRef || it is TimeWindow })
}
val response = try {
@ -90,19 +91,19 @@ object NotaryFlow {
/**
* A flow run by a notary service that handles notarisation requests.
*
* It checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
* It checks that the time-window command is valid (if present) and commits the input state, or returns a conflict
* if any of the input states have been previously committed.
*
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
*/
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
abstract class Service(val otherSide: Party,
val timestampChecker: TimestampChecker,
val timeWindowChecker: TimeWindowChecker,
val uniquenessProvider: UniquenessProvider) : FlowLogic<Void?>() {
@Suspendable
override fun call(): Void? {
val (id, inputs, timestamp) = receiveAndVerifyTx()
validateTimestamp(timestamp)
val (id, inputs, timeWindow) = receiveAndVerifyTx()
validateTimeWindow(timeWindow)
commitInputStates(inputs, id)
signAndSendResponse(id)
return null
@ -121,9 +122,9 @@ object NotaryFlow {
send(otherSide, listOf(signature))
}
private fun validateTimestamp(t: Timestamp?) {
if (t != null && !timestampChecker.isValid(t))
throw NotaryException(NotaryError.TimestampInvalid)
private fun validateTimeWindow(t: TimeWindow?) {
if (t != null && !timeWindowChecker.isValid(t))
throw NotaryException(NotaryError.TimeWindowInvalid)
}
/**
@ -162,7 +163,7 @@ object NotaryFlow {
* The minimum amount of information needed to notarise a transaction. Note that this does not include
* any sensitive transaction details.
*/
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: Timestamp?)
data class TransactionParts(val id: SecureHash, val inputs: List<StateRef>, val timestamp: TimeWindow?)
class NotaryException(val error: NotaryError) : FlowException("Error response from Notary - $error")
@ -172,8 +173,8 @@ sealed class NotaryError {
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
}
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
object TimestampInvalid : NotaryError()
/** Thrown if the time specified in the [TimeWindow] command is outside the allowed tolerance. */
object TimeWindowInvalid : NotaryError()
data class TransactionInvalid(val msg: String) : NotaryError()
data class SignaturesInvalid(val msg: String) : NotaryError()

View File

@ -0,0 +1,89 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.flows.InitiatedBy
import net.corda.core.flows.InitiatingFlow
import net.corda.core.flows.StartableByRPC
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.unwrap
import org.bouncycastle.cert.X509CertificateHolder
import java.security.cert.CertPath
/**
* Very basic flow which exchanges transaction key and certificate paths between two parties in a transaction.
* This is intended for use as a subflow of another flow.
*/
object TxKeyFlow {
abstract class AbstractIdentityFlow<out T>(val otherSide: Party, val revocationEnabled: Boolean): FlowLogic<T>() {
fun validateIdentity(untrustedIdentity: AnonymousIdentity): AnonymousIdentity {
val (certPath, theirCert, txIdentity) = untrustedIdentity
if (theirCert.subject == otherSide.name) {
serviceHub.identityService.registerAnonymousIdentity(txIdentity, otherSide, certPath)
return AnonymousIdentity(certPath, theirCert, txIdentity)
} else
throw IllegalStateException("Expected certificate subject to be ${otherSide.name} but found ${theirCert.subject}")
}
}
@StartableByRPC
@InitiatingFlow
class Requester(otherSide: Party,
override val progressTracker: ProgressTracker) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide, false) {
constructor(otherSide: Party) : this(otherSide, tracker())
companion object {
object AWAITING_KEY : ProgressTracker.Step("Awaiting key")
fun tracker() = ProgressTracker(AWAITING_KEY)
}
@Suspendable
override fun call(): Map<Party, AnonymousIdentity> {
progressTracker.currentStep = AWAITING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) }
send(otherSide, myIdentity)
return mapOf(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
}
}
/**
* Flow which waits for a key request from a counterparty, generates a new key and then returns it to the
* counterparty and as the result from the flow.
*/
@InitiatedBy(Requester::class)
class Provider(otherSide: Party) : AbstractIdentityFlow<Map<Party, AnonymousIdentity>>(otherSide, false) {
companion object {
object SENDING_KEY : ProgressTracker.Step("Sending key")
}
override val progressTracker: ProgressTracker = ProgressTracker(SENDING_KEY)
@Suspendable
override fun call(): Map<Party, AnonymousIdentity> {
val revocationEnabled = false
progressTracker.currentStep = SENDING_KEY
val myIdentityFragment = serviceHub.keyManagementService.freshKeyAndCert(serviceHub.myInfo.legalIdentityAndCert, revocationEnabled)
val myIdentity = AnonymousIdentity(myIdentityFragment)
send(otherSide, myIdentity)
val theirIdentity = receive<AnonymousIdentity>(otherSide).unwrap { validateIdentity(it) }
return mapOf(Pair(otherSide, myIdentity),
Pair(serviceHub.myInfo.legalIdentity, theirIdentity))
}
}
@CordaSerializable
data class AnonymousIdentity(
val certPath: CertPath,
val certificate: X509CertificateHolder,
val identity: AnonymousParty) {
constructor(myIdentity: Pair<X509CertificateHolder, CertPath>) : this(myIdentity.second,
myIdentity.first,
AnonymousParty(myIdentity.second.certificates.first().publicKey))
}
}

View File

@ -1,41 +0,0 @@
package net.corda.flows
import co.paralleluniverse.fibers.Suspendable
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.utilities.unwrap
import java.security.PublicKey
import java.security.cert.Certificate
object TxKeyFlowUtilities {
/**
* Receive a key from a counterparty. This would normally be triggered by a flow as part of a transaction assembly
* process.
*/
@Suspendable
fun receiveKey(flow: FlowLogic<*>, otherSide: Party): Pair<PublicKey, Certificate?> {
val untrustedKey = flow.receive<ProvidedTransactionKey>(otherSide)
return untrustedKey.unwrap {
// TODO: Verify the certificate connects the given key to the counterparty, once we have certificates
Pair(it.key, it.certificate)
}
}
/**
* Generates a new key and then returns it to the counterparty and as the result from the function. Note that this
* is an expensive operation, and should only be called once the calling flow has confirmed it wants to be part of
* a transaction with the counterparty, in order to avoid a DoS risk.
*/
@Suspendable
fun provideKey(flow: FlowLogic<*>, otherSide: Party): PublicKey {
val key = flow.serviceHub.keyManagementService.freshKey()
// TODO: Generate and sign certificate for the key, once we have signing support for composite keys
// (in this case the legal identity key)
flow.send(otherSide, ProvidedTransactionKey(key, null))
return key
}
@CordaSerializable
data class ProvidedTransactionKey(val key: PublicKey, val certificate: Certificate?)
}

View File

@ -1,42 +1,43 @@
package net.corda.core.flows;
import co.paralleluniverse.fibers.*;
import co.paralleluniverse.fibers.Suspendable;
import net.corda.core.identity.Party;
import net.corda.testing.node.*;
import org.junit.*;
import net.corda.testing.node.MockNetwork;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.util.concurrent.*;
import java.util.concurrent.Future;
import static org.assertj.core.api.AssertionsForClassTypes.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
public class FlowsInJavaTest {
private final MockNetwork net = new MockNetwork();
private final MockNetwork mockNet = new MockNetwork();
private MockNetwork.MockNode node1;
private MockNetwork.MockNode node2;
@Before
public void setUp() {
MockNetwork.BasketOfNodes someNodes = net.createSomeNodes(2);
MockNetwork.BasketOfNodes someNodes = mockNet.createSomeNodes(2);
node1 = someNodes.getPartyNodes().get(0);
node2 = someNodes.getPartyNodes().get(1);
net.runNetwork();
mockNet.runNetwork();
}
@After
public void cleanUp() {
net.stopNodes();
mockNet.stopNodes();
}
@Test
public void suspendableActionInsideUnwrap() throws Exception {
node2.getServices().registerServiceFlow(SendInUnwrapFlow.class, (otherParty) -> new OtherFlow(otherParty, "Hello"));
node2.registerInitiatedFlow(SendHelloAndThenReceive.class);
Future<String> result = node1.getServices().startFlow(new SendInUnwrapFlow(node2.getInfo().getLegalIdentity())).getResultFuture();
net.runNetwork();
mockNet.runNetwork();
assertThat(result.get()).isEqualTo("Hello");
}
@SuppressWarnings("unused")
@InitiatingFlow
private static class SendInUnwrapFlow extends FlowLogic<String> {
private final Party otherParty;
@ -55,19 +56,18 @@ public class FlowsInJavaTest {
}
}
private static class OtherFlow extends FlowLogic<String> {
@InitiatedBy(SendInUnwrapFlow.class)
private static class SendHelloAndThenReceive extends FlowLogic<String> {
private final Party otherParty;
private final String payload;
private OtherFlow(Party otherParty, String payload) {
private SendHelloAndThenReceive(Party otherParty) {
this.otherParty = otherParty;
this.payload = payload;
}
@Suspendable
@Override
public String call() throws FlowException {
return sendAndReceive(String.class, otherParty, payload).unwrap(data -> data);
return sendAndReceive(String.class, otherParty, "Hello").unwrap(data -> data);
}
}

View File

@ -0,0 +1,42 @@
package net.corda.core
import org.junit.Assert.assertArrayEquals
import org.junit.Test
import java.util.stream.IntStream
import java.util.stream.Stream
import kotlin.test.assertEquals
class StreamsTest {
@Test
fun `IntProgression stream works`() {
assertArrayEquals(intArrayOf(1, 2, 3, 4), (1..4).stream().toArray())
assertArrayEquals(intArrayOf(1, 2, 3, 4), (1 until 5).stream().toArray())
assertArrayEquals(intArrayOf(1, 3), (1..4 step 2).stream().toArray())
assertArrayEquals(intArrayOf(1, 3), (1..3 step 2).stream().toArray())
assertArrayEquals(intArrayOf(), (1..0).stream().toArray())
assertArrayEquals(intArrayOf(1, 0), (1 downTo 0).stream().toArray())
assertArrayEquals(intArrayOf(3, 1), (3 downTo 0 step 2).stream().toArray())
assertArrayEquals(intArrayOf(3, 1), (3 downTo 1 step 2).stream().toArray())
}
@Test
fun `IntProgression spliterator characteristics and comparator`() {
val rangeCharacteristics = IntStream.range(0, 2).spliterator().characteristics()
val forward = (0..9 step 3).stream().spliterator()
assertEquals(rangeCharacteristics, forward.characteristics())
assertEquals(null, forward.comparator)
val reverse = (9 downTo 0 step 3).stream().spliterator()
assertEquals(rangeCharacteristics, reverse.characteristics())
assertEquals(Comparator.reverseOrder(), reverse.comparator)
}
@Test
fun `Stream toTypedArray works`() {
val a: Array<String> = Stream.of("one", "two").toTypedArray()
assertEquals(Array<String>::class.java, a.javaClass)
assertArrayEquals(arrayOf("one", "two"), a)
val b: Array<String?> = Stream.of("one", "two", null).toTypedArray()
assertEquals(Array<String?>::class.java, b.javaClass)
assertArrayEquals(arrayOf("one", "two", null), b)
}
}

View File

@ -1,10 +1,18 @@
package net.corda.core
import com.google.common.util.concurrent.MoreExecutors
import com.nhaarman.mockito_kotlin.mock
import com.nhaarman.mockito_kotlin.same
import com.nhaarman.mockito_kotlin.verify
import org.assertj.core.api.Assertions.*
import org.junit.Test
import org.mockito.ArgumentMatchers.anyString
import org.slf4j.Logger
import rx.subjects.PublishSubject
import java.util.*
import java.util.concurrent.CancellationException
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class UtilsTest {
@Test
@ -57,4 +65,17 @@ class UtilsTest {
future.get()
}
}
}
@Test
fun `andForget works`() {
val log = mock<Logger>()
val throwable = Exception("Boom")
val executor = MoreExecutors.listeningDecorator(Executors.newSingleThreadExecutor())
executor.submit { throw throwable }.andForget(log)
executor.shutdown()
while (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
// Do nothing.
}
verify(log).error(anyString(), same(throwable))
}
}

View File

@ -20,6 +20,12 @@ class AmountTests {
assertEquals(expected, amount.quantity)
}
@Test
fun `make sure Amount has decimal places`() {
val x = Amount(1, Currency.getInstance("USD"))
assertTrue("0.01" in x.toString())
}
@Test
fun decimalConversion() {
val quantity = 1234L

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