mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge branch 'master' into mike-enterprise-merge-june-20th
This commit is contained in:
commit
ca8f5050cf
2
.gitignore
vendored
2
.gitignore
vendored
@ -84,7 +84,7 @@ crashlytics-build.properties
|
||||
docs/virtualenv/
|
||||
|
||||
# bft-smart
|
||||
config/currentView
|
||||
**/config/currentView
|
||||
|
||||
# vim
|
||||
*.swp
|
||||
|
6
.idea/compiler.xml
generated
6
.idea/compiler.xml
generated
@ -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" />
|
||||
|
@ -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)
|
||||
|
||||
|
19
build.gradle
19
build.gradle
@ -5,7 +5,7 @@ buildscript {
|
||||
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
|
||||
|
||||
// Our version: bump this on release.
|
||||
ext.corda_release_version = "0.12-SNAPSHOT"
|
||||
ext.corda_release_version = "0.13-SNAPSHOT"
|
||||
// Increment this on any release that changes public APIs anywhere in the Corda platform
|
||||
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
|
||||
ext.corda_platform_version = 1
|
||||
@ -34,7 +34,7 @@ buildscript {
|
||||
ext.guava_version = constants.getProperty("guavaVersion")
|
||||
ext.quickcheck_version = '0.7'
|
||||
ext.okhttp_version = '3.5.0'
|
||||
ext.netty_version = '4.1.5.Final'
|
||||
ext.netty_version = '4.1.9.Final'
|
||||
ext.typesafe_config_version = constants.getProperty("typesafeConfigVersion")
|
||||
ext.fileupload_version = '1.3.2'
|
||||
ext.junit_version = '4.12'
|
||||
@ -45,7 +45,7 @@ buildscript {
|
||||
ext.h2_version = '1.4.194'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.2.1'
|
||||
ext.dokka_version = '0.9.13'
|
||||
ext.dokka_version = '0.9.14'
|
||||
ext.eddsa_version = '0.2.0'
|
||||
|
||||
// Update 121 is required for ObjectInputFilter and at time of writing 131 was latest:
|
||||
@ -219,17 +219,15 @@ tasks.withType(Test) {
|
||||
|
||||
task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
directory "./build/nodes"
|
||||
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||
networkMap "CN=Controller,O=R3,OU=corda,L=London,C=GB"
|
||||
node {
|
||||
name "CN=Controller,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "London"
|
||||
name "CN=Controller,O=R3,OU=corda,L=London,C=GB"
|
||||
advertisedServices = ["corda.notary.validating"]
|
||||
p2pPort 10002
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "CN=Bank A,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "London"
|
||||
name "CN=Bank A,O=R3,OU=corda,L=London,C=GB"
|
||||
advertisedServices = []
|
||||
p2pPort 10012
|
||||
rpcPort 10013
|
||||
@ -237,8 +235,7 @@ task deployNodes(type: net.corda.plugins.Cordform, dependsOn: ['jar']) {
|
||||
cordapps = []
|
||||
}
|
||||
node {
|
||||
name "CN=Bank B,O=R3,OU=corda,L=London,C=UK"
|
||||
nearestCity "New York"
|
||||
name "CN=Bank B,O=R3,OU=corda,L=London,C=GB"
|
||||
advertisedServices = []
|
||||
p2pPort 10007
|
||||
rpcPort 10008
|
||||
@ -257,7 +254,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||
publications = ['jfx', 'mock', 'rpc', 'core', 'corda', 'cordform-common', 'corda-webserver', 'finance', 'node', 'node-api', 'node-schemas', 'test-utils', 'jackson', 'verifier', 'webserver']
|
||||
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
|
||||
license {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
|
@ -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
|
||||
}
|
@ -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}")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -23,3 +23,11 @@ dependencies {
|
||||
|
||||
testCompile project(':test-utils')
|
||||
}
|
||||
|
||||
jar {
|
||||
baseName 'corda-mock'
|
||||
}
|
||||
|
||||
publish {
|
||||
name = jar.baseName
|
||||
}
|
@ -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
|
||||
}
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
*
|
||||
|
@ -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
|
||||
}
|
||||
|
30
core/src/main/kotlin/net/corda/core/Streams.kt
Normal file
30
core/src/main/kotlin/net/corda/core/Streams.kt
Normal 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>
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
@ -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 /////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
31
core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt
Normal file
31
core/src/main/kotlin/net/corda/core/flows/FlowInitiator.kt
Normal 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"
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
16
core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt
Normal file
16
core/src/main/kotlin/net/corda/core/flows/InitiatedBy.kt
Normal 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<*>>)
|
@ -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)
|
||||
|
@ -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
|
@ -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]"
|
||||
}
|
@ -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))
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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()
|
||||
}
|
@ -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
|
||||
}
|
@ -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>
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
||||
/**
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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]) })
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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])
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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 = "?"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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)"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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?
|
||||
)
|
||||
}
|
||||
|
||||
|
103
core/src/main/kotlin/net/corda/core/utilities/CordaException.kt
Normal file
103
core/src/main/kotlin/net/corda/core/utilities/CordaException.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
89
core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt
Normal file
89
core/src/main/kotlin/net/corda/flows/TxKeyFlow.kt
Normal 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))
|
||||
}
|
||||
}
|
@ -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?)
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
42
core/src/test/kotlin/net/corda/core/StreamsTest.kt
Normal file
42
core/src/test/kotlin/net/corda/core/StreamsTest.kt
Normal 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)
|
||||
}
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user