mirror of
https://github.com/corda/corda.git
synced 2024-12-29 09:18:58 +00:00
Merge open/master
This commit is contained in:
commit
d52accb52c
4
.gitignore
vendored
4
.gitignore
vendored
@ -77,3 +77,7 @@ crashlytics-build.properties
|
|||||||
|
|
||||||
# docs related
|
# docs related
|
||||||
docs/virtualenv/
|
docs/virtualenv/
|
||||||
|
|
||||||
|
# bft-smart
|
||||||
|
node/bft-smart-config/currentView
|
||||||
|
node/config/currentView
|
||||||
|
11
.idea/dictionaries/rossnicoll.xml
generated
11
.idea/dictionaries/rossnicoll.xml
generated
@ -1,11 +0,0 @@
|
|||||||
<component name="ProjectDictionaryState">
|
|
||||||
<dictionary name="rossnicoll">
|
|
||||||
<words>
|
|
||||||
<w>Corda</w>
|
|
||||||
<w>Kryo</w>
|
|
||||||
<w>Quasar</w>
|
|
||||||
<w>blockchain</w>
|
|
||||||
<w>counterparty</w>
|
|
||||||
</words>
|
|
||||||
</dictionary>
|
|
||||||
</component>
|
|
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
|||||||
Copyright 2016, R3 Limited.
|
Copyright 2016 - 2017, R3 Limited.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -55,6 +55,9 @@ To look at the Corda source and run some sample applications:
|
|||||||
* [Documentation](https://docs.corda.net)
|
* [Documentation](https://docs.corda.net)
|
||||||
* [Slack channel] (https://slack.corda.net/)
|
* [Slack channel] (https://slack.corda.net/)
|
||||||
* [Forum](https://discourse.corda.net)
|
* [Forum](https://discourse.corda.net)
|
||||||
|
* [Meetups](https://www.meetup.com/pro/corda/)
|
||||||
|
* [Training Course](https://www.corda.net/corda-training/)
|
||||||
|
|
||||||
|
|
||||||
## Development State
|
## Development State
|
||||||
|
|
||||||
@ -77,7 +80,7 @@ Please read [here](./CONTRIBUTING.md).
|
|||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
[Apache 2.0](./LICENCE)
|
[Apache 2.0](./LICENSE)
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
26
build.gradle
26
build.gradle
@ -4,16 +4,16 @@ buildscript {
|
|||||||
file("publish.properties").withInputStream { props.load(it) }
|
file("publish.properties").withInputStream { props.load(it) }
|
||||||
|
|
||||||
// Our version: bump this on release.
|
// Our version: bump this on release.
|
||||||
ext.corda_version = "0.9-SNAPSHOT"
|
ext.corda_version = "0.10-SNAPSHOT"
|
||||||
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
|
ext.gradle_plugins_version = props.getProperty("gradlePluginsVersion")
|
||||||
|
|
||||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||||
//
|
//
|
||||||
// TODO: Sort this alphabetically.
|
// TODO: Sort this alphabetically.
|
||||||
ext.kotlin_version = '1.0.5-2'
|
ext.kotlin_version = '1.0.6'
|
||||||
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
ext.quasar_version = '0.7.6' // TODO: Upgrade to 0.7.7+ when Quasar bug 238 is resolved.
|
||||||
ext.asm_version = '0.5.3'
|
ext.asm_version = '0.5.3'
|
||||||
ext.artemis_version = '1.5.1'
|
ext.artemis_version = '1.5.3'
|
||||||
ext.jackson_version = '2.8.5'
|
ext.jackson_version = '2.8.5'
|
||||||
ext.jetty_version = '9.3.9.v20160517'
|
ext.jetty_version = '9.3.9.v20160517'
|
||||||
ext.jersey_version = '2.25'
|
ext.jersey_version = '2.25'
|
||||||
@ -24,12 +24,15 @@ buildscript {
|
|||||||
ext.guava_version = '19.0'
|
ext.guava_version = '19.0'
|
||||||
ext.quickcheck_version = '0.7'
|
ext.quickcheck_version = '0.7'
|
||||||
ext.okhttp_version = '3.5.0'
|
ext.okhttp_version = '3.5.0'
|
||||||
|
ext.netty_version = '4.1.5.Final'
|
||||||
ext.typesafe_config_version = '1.3.1'
|
ext.typesafe_config_version = '1.3.1'
|
||||||
ext.junit_version = '4.12'
|
ext.junit_version = '4.12'
|
||||||
ext.jopt_simple_version = '5.0.2'
|
ext.jopt_simple_version = '5.0.2'
|
||||||
ext.jansi_version = '1.14'
|
ext.jansi_version = '1.14'
|
||||||
ext.hibernate_version = '5.2.6.Final'
|
ext.hibernate_version = '5.2.6.Final'
|
||||||
ext.h2_version = '1.4.193'
|
ext.h2_version = '1.4.193'
|
||||||
|
ext.rxjava_version = '1.2.4'
|
||||||
|
ext.requery_version = '1.1.1'
|
||||||
ext.dokka_version = '0.9.13'
|
ext.dokka_version = '0.9.13'
|
||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
@ -44,7 +47,9 @@ buildscript {
|
|||||||
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
||||||
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
||||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
||||||
|
classpath "org.jetbrains.kotlin:kotlin-noarg:$kotlin_version"
|
||||||
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
|
||||||
|
classpath "org.ajoberstar:grgit:1.1.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +59,10 @@ plugins {
|
|||||||
id "us.kirchmeier.capsule" version "1.0.2"
|
id "us.kirchmeier.capsule" version "1.0.2"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
|
||||||
|
}
|
||||||
|
|
||||||
apply plugin: 'kotlin'
|
apply plugin: 'kotlin'
|
||||||
apply plugin: 'project-report'
|
apply plugin: 'project-report'
|
||||||
apply plugin: 'com.github.ben-manes.versions'
|
apply plugin: 'com.github.ben-manes.versions'
|
||||||
@ -84,6 +93,10 @@ allprojects {
|
|||||||
|
|
||||||
group 'net.corda'
|
group 'net.corda'
|
||||||
version "$corda_version"
|
version "$corda_version"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
maven { url 'https://jitpack.io' }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
|
// Check that we are running on a Java 8 JDK. The source/targetCompatibility values above aren't sufficient to
|
||||||
@ -107,6 +120,7 @@ dependencies {
|
|||||||
compile project(':node')
|
compile project(':node')
|
||||||
compile "com.google.guava:guava:$guava_version"
|
compile "com.google.guava:guava:$guava_version"
|
||||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||||
|
runtime project(path: ":node:webserver", configuration: 'runtimeArtifacts')
|
||||||
}
|
}
|
||||||
|
|
||||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||||
@ -172,7 +186,7 @@ bintrayConfig {
|
|||||||
projectUrl = 'https://github.com/corda/corda'
|
projectUrl = 'https://github.com/corda/corda'
|
||||||
gpgSign = true
|
gpgSign = true
|
||||||
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
|
||||||
publications = ['client', 'core', 'corda', 'finance', 'node', 'test-utils']
|
publications = ['client', 'core', 'corda', 'corda-webserver', 'finance', 'node', 'node-schemas', 'test-utils', 'jackson']
|
||||||
license {
|
license {
|
||||||
name = 'Apache-2.0'
|
name = 'Apache-2.0'
|
||||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||||
@ -191,7 +205,7 @@ dokka {
|
|||||||
moduleName = 'corda'
|
moduleName = 'corda'
|
||||||
outputDirectory = 'docs/build/html/api/kotlin'
|
outputDirectory = 'docs/build/html/api/kotlin'
|
||||||
processConfigurations = ['compile']
|
processConfigurations = ['compile']
|
||||||
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
|
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
|
||||||
}
|
}
|
||||||
|
|
||||||
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||||
@ -199,7 +213,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
|||||||
outputFormat = "javadoc"
|
outputFormat = "javadoc"
|
||||||
outputDirectory = 'docs/build/html/api/javadoc'
|
outputDirectory = 'docs/build/html/api/javadoc'
|
||||||
processConfigurations = ['compile']
|
processConfigurations = ['compile']
|
||||||
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin')
|
sourceDirs = files('core/src/main/kotlin', 'client/src/main/kotlin', 'node/src/main/kotlin', 'finance/src/main/kotlin', 'client/jackson/src/main/kotlin')
|
||||||
}
|
}
|
||||||
|
|
||||||
task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
|
task apidocs(dependsOn: ['dokka', 'dokkaJavadoc'])
|
19
client/jackson/build.gradle
Normal file
19
client/jackson/build.gradle
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
apply plugin: 'java'
|
||||||
|
apply plugin: 'kotlin'
|
||||||
|
apply plugin: 'net.corda.plugins.publish-utils'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenLocal()
|
||||||
|
mavenCentral()
|
||||||
|
jcenter()
|
||||||
|
maven {
|
||||||
|
url 'https://dl.bintray.com/kotlin/exposed'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile project(':core')
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||||
|
compile ("com.fasterxml.jackson.module:jackson-module-kotlin:${jackson_version}")
|
||||||
|
testCompile "junit:junit:$junit_version"
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package net.corda.node.utilities
|
package net.corda.jackson
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonGenerator
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
import com.fasterxml.jackson.core.JsonParseException
|
import com.fasterxml.jackson.core.JsonParseException
|
||||||
@ -13,10 +13,10 @@ import net.corda.core.contracts.BusinessCalendar
|
|||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
|
import net.corda.core.node.services.IdentityService
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import net.corda.core.node.services.IdentityService
|
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
@ -25,21 +25,27 @@ import java.time.LocalDateTime
|
|||||||
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
* Utilities and serialisers for working with JSON representations of basic types. This adds Jackson support for
|
||||||
* the java.time API, some core types, and Kotlin data classes.
|
* the java.time API, some core types, and Kotlin data classes.
|
||||||
*
|
*
|
||||||
* TODO: This does not belong in node. It should be moved to the client module or a dedicated webserver module.
|
* Note that Jackson can also be used to serialise/deserialise other formats such as Yaml and XML.
|
||||||
*/
|
*/
|
||||||
object JsonSupport {
|
object JacksonSupport {
|
||||||
|
// If you change this API please update the docs in the docsite (json.rst)
|
||||||
|
|
||||||
interface PartyObjectMapper {
|
interface PartyObjectMapper {
|
||||||
fun partyFromName(partyName: String): Party?
|
fun partyFromName(partyName: String): Party?
|
||||||
|
fun partyFromKey(owningKey: CompositeKey): Party?
|
||||||
}
|
}
|
||||||
|
|
||||||
class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
|
class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
|
||||||
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
override fun partyFromName(partyName: String): Party? = rpc.partyFromName(partyName)
|
||||||
|
override fun partyFromKey(owningKey: CompositeKey): Party? = rpc.partyFromKey(owningKey)
|
||||||
}
|
}
|
||||||
class IdentityObjectMapper(val identityService: IdentityService) : PartyObjectMapper, ObjectMapper(){
|
class IdentityObjectMapper(val identityService: IdentityService) : PartyObjectMapper, ObjectMapper(){
|
||||||
override fun partyFromName(partyName: String) = identityService.partyFromName(partyName)
|
override fun partyFromName(partyName: String): Party? = identityService.partyFromName(partyName)
|
||||||
|
override fun partyFromKey(owningKey: CompositeKey): Party? = identityService.partyFromKey(owningKey)
|
||||||
}
|
}
|
||||||
class NoPartyObjectMapper: PartyObjectMapper, ObjectMapper() {
|
class NoPartyObjectMapper: PartyObjectMapper, ObjectMapper() {
|
||||||
override fun partyFromName(partyName: String) = throw UnsupportedOperationException()
|
override fun partyFromName(partyName: String): Party? = throw UnsupportedOperationException()
|
||||||
|
override fun partyFromKey(owningKey: CompositeKey): Party? = throw UnsupportedOperationException()
|
||||||
}
|
}
|
||||||
|
|
||||||
val javaTimeModule: Module by lazy {
|
val javaTimeModule: Module by lazy {
|
||||||
@ -53,6 +59,8 @@ object JsonSupport {
|
|||||||
|
|
||||||
val cordaModule: Module by lazy {
|
val cordaModule: Module by lazy {
|
||||||
SimpleModule("core").apply {
|
SimpleModule("core").apply {
|
||||||
|
addSerializer(AnonymousParty::class.java, AnonymousPartySerializer)
|
||||||
|
addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer)
|
||||||
addSerializer(Party::class.java, PartySerializer)
|
addSerializer(Party::class.java, PartySerializer)
|
||||||
addDeserializer(Party::class.java, PartyDeserializer)
|
addDeserializer(Party::class.java, PartyDeserializer)
|
||||||
addSerializer(BigDecimal::class.java, ToStringSerializer)
|
addSerializer(BigDecimal::class.java, ToStringSerializer)
|
||||||
@ -78,13 +86,16 @@ object JsonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mapper requiring RPC support to deserialise parties from names */
|
/** Mapper requiring RPC support to deserialise parties from names */
|
||||||
|
@JvmStatic
|
||||||
fun createDefaultMapper(rpc: CordaRPCOps): ObjectMapper = configureMapper(RpcObjectMapper(rpc))
|
fun createDefaultMapper(rpc: CordaRPCOps): ObjectMapper = configureMapper(RpcObjectMapper(rpc))
|
||||||
|
|
||||||
/* For testing or situations where deserialising parties is not required */
|
/** For testing or situations where deserialising parties is not required */
|
||||||
|
@JvmStatic
|
||||||
fun createNonRpcMapper(): ObjectMapper = configureMapper(NoPartyObjectMapper())
|
fun createNonRpcMapper(): ObjectMapper = configureMapper(NoPartyObjectMapper())
|
||||||
|
|
||||||
/* For testing with an in memory identity service */
|
/** For testing with an in memory identity service */
|
||||||
|
@JvmStatic
|
||||||
fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
|
fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
|
||||||
|
|
||||||
private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
|
private fun configureMapper(mapper: ObjectMapper): ObjectMapper = mapper.apply {
|
||||||
@ -120,6 +131,25 @@ object JsonSupport {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object AnonymousPartySerializer : JsonSerializer<AnonymousParty>() {
|
||||||
|
override fun serialize(obj: AnonymousParty, generator: JsonGenerator, provider: SerializerProvider) {
|
||||||
|
generator.writeString(obj.owningKey.toBase58String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object AnonymousPartyDeserializer : JsonDeserializer<AnonymousParty>() {
|
||||||
|
override fun deserialize(parser: JsonParser, context: DeserializationContext): AnonymousParty {
|
||||||
|
if (parser.currentToken == JsonToken.FIELD_NAME) {
|
||||||
|
parser.nextToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
val mapper = parser.codec as PartyObjectMapper
|
||||||
|
// TODO this needs to use some industry identifier(s) instead of these keys
|
||||||
|
val key = CompositeKey.parseFromBase58(parser.text)
|
||||||
|
return AnonymousParty(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
object PartySerializer : JsonSerializer<Party>() {
|
object PartySerializer : JsonSerializer<Party>() {
|
||||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||||
generator.writeString(obj.name)
|
generator.writeString(obj.name)
|
||||||
@ -224,3 +254,4 @@ object JsonSupport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,21 +1,18 @@
|
|||||||
package net.corda.node
|
package net.corda.jackson
|
||||||
|
|
||||||
import com.pholser.junit.quickcheck.From
|
import com.pholser.junit.quickcheck.From
|
||||||
import com.pholser.junit.quickcheck.Property
|
import com.pholser.junit.quickcheck.Property
|
||||||
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
||||||
import net.corda.core.testing.PublicKeyGenerator
|
import net.corda.core.testing.PublicKeyGenerator
|
||||||
import net.corda.node.utilities.JsonSupport
|
|
||||||
import net.corda.testing.node.MockIdentityService
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
import kotlin.test.assertEquals
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
@RunWith(JUnitQuickcheck::class)
|
@RunWith(JUnitQuickcheck::class)
|
||||||
class JsonSupportTest {
|
class JacksonSupportTest {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val mapper = JsonSupport.createNonRpcMapper()
|
val mapper = JacksonSupport.createNonRpcMapper()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Property
|
@Property
|
@ -176,14 +176,16 @@ fun <A : Any> Generator.Companion.replicatePoisson(meanSize: Double, generator:
|
|||||||
fun <A : Any> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
|
fun <A : Any> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
|
||||||
fun <A : Any> Generator.Companion.pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
fun <A : Any> Generator.Companion.pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
||||||
val mask = BitSet(list.size)
|
val mask = BitSet(list.size)
|
||||||
for (i in 0..Math.min(list.size, number) - 1) {
|
val size = Math.min(list.size, number)
|
||||||
mask[i] = 1
|
for (i in 0..size - 1) {
|
||||||
|
// mask[i] = 1 desugars into mask.set(i, 1), which sets a range instead of a bit
|
||||||
|
mask[i] = true
|
||||||
}
|
}
|
||||||
for (i in 0..mask.size() - 1) {
|
for (i in 0..list.size - 1) {
|
||||||
val byte = mask[i]
|
val bit = mask[i]
|
||||||
val swapIndex = i + it.nextInt(mask.size() - i)
|
val swapIndex = i + it.nextInt(size - i)
|
||||||
mask[i] = mask[swapIndex]
|
mask[i] = mask[swapIndex]
|
||||||
mask[swapIndex] = byte
|
mask[swapIndex] = bit
|
||||||
}
|
}
|
||||||
val resultList = ArrayList<A>()
|
val resultList = ArrayList<A>()
|
||||||
list.forEachIndexed { index, a ->
|
list.forEachIndexed { index, a ->
|
||||||
|
@ -40,6 +40,7 @@ class NetworkIdentityModel {
|
|||||||
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
return advertisedServices.any { it.info.type == NetworkMapService.type || it.info.type.isNotary() }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Use Identity Service in service hub instead?
|
||||||
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
|
fun lookup(compositeKey: CompositeKey): ObservableValue<NodeInfo?> = parties.firstOrDefault(notaries.firstOrNullObservable { it.notaryIdentity.owningKey == compositeKey }) {
|
||||||
it.legalIdentity.owningKey == compositeKey
|
it.legalIdentity.owningKey == compositeKey
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ keyStorePassword : "cordacadevpass"
|
|||||||
trustStorePassword : "trustpass"
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:31337"
|
artemisAddress : "localhost:31337"
|
||||||
webAddress : "localhost:31339"
|
webAddress : "localhost:31339"
|
||||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||||
networkMapService : {
|
networkMapService : {
|
||||||
address : "localhost:12345"
|
address : "localhost:12345"
|
||||||
legalName : "Network Map Service"
|
legalName : "Network Map Service"
|
||||||
|
@ -4,7 +4,7 @@ keyStorePassword : "cordacadevpass"
|
|||||||
trustStorePassword : "trustpass"
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:31338"
|
artemisAddress : "localhost:31338"
|
||||||
webAddress : "localhost:31340"
|
webAddress : "localhost:31340"
|
||||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||||
networkMapService : {
|
networkMapService : {
|
||||||
address : "localhost:12345"
|
address : "localhost:12345"
|
||||||
legalName : "Network Map Service"
|
legalName : "Network Map Service"
|
||||||
|
@ -6,17 +6,14 @@
|
|||||||
<Property name="log-name">node-${hostName}</Property>
|
<Property name="log-name">node-${hostName}</Property>
|
||||||
<Property name="archive">${sys:log-path}/archive</Property>
|
<Property name="archive">${sys:log-path}/archive</Property>
|
||||||
<Property name="consoleLogLevel">error</Property>
|
<Property name="consoleLogLevel">error</Property>
|
||||||
|
<Property name="defaultLogLevel">info</Property>
|
||||||
</Properties>
|
</Properties>
|
||||||
|
|
||||||
<ThresholdFilter level="trace"/>
|
<ThresholdFilter level="trace"/>
|
||||||
|
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||||
<PatternLayout>
|
<PatternLayout pattern="%highlight{%level{length=1} %d{HH:mm:ss} [%t] %c{2}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" />
|
||||||
<pattern>
|
|
||||||
%highlight{%level{length=1} %d{HH:mm:ss} %T %c{1}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red blink}
|
|
||||||
</pattern>>
|
|
||||||
</PatternLayout>
|
|
||||||
</Console>
|
</Console>
|
||||||
|
|
||||||
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
<!-- Will generate up to 10 log files for a given day. During every rollover it will delete
|
||||||
@ -25,7 +22,7 @@
|
|||||||
fileName="${sys:log-path}/${log-name}.log"
|
fileName="${sys:log-path}/${log-name}.log"
|
||||||
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
|
filePattern="${archive}/${log-name}.%d{yyyy-MM-dd}-%i.log.gz">
|
||||||
|
|
||||||
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{1} - %msg%n"/>
|
<PatternLayout pattern="[%-5level] %d{ISO8601}{GMT+0} [%t] %c{2}.%M - %msg%n"/>
|
||||||
|
|
||||||
<Policies>
|
<Policies>
|
||||||
<TimeBasedTriggeringPolicy/>
|
<TimeBasedTriggeringPolicy/>
|
||||||
@ -47,13 +44,9 @@
|
|||||||
</Appenders>
|
</Appenders>
|
||||||
|
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="info">
|
<Root level="${sys:defaultLogLevel}">
|
||||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||||
<AppenderRef ref="RollingFile-Appender" level="info"/>
|
<AppenderRef ref="RollingFile-Appender" />
|
||||||
</Root>
|
</Root>
|
||||||
<Logger name="net.corda" level="info" additivity="false">
|
|
||||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
|
||||||
<AppenderRef ref="RollingFile-Appender"/>
|
|
||||||
</Logger>
|
|
||||||
</Loggers>
|
</Loggers>
|
||||||
</Configuration>
|
</Configuration>
|
@ -4,5 +4,5 @@ keyStorePassword : "cordacadevpass"
|
|||||||
trustStorePassword : "trustpass"
|
trustStorePassword : "trustpass"
|
||||||
artemisAddress : "localhost:12345"
|
artemisAddress : "localhost:12345"
|
||||||
webAddress : "localhost:12346"
|
webAddress : "localhost:12346"
|
||||||
extraAdvertisedServiceIds: "corda.notary.validating"
|
extraAdvertisedServiceIds : [ "corda.notary.validating" ]
|
||||||
useHTTPS : false
|
useHTTPS : false
|
@ -1,19 +1,18 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Configuration status="info">
|
<Configuration status="info">
|
||||||
|
<Properties>
|
||||||
|
<Property name="defaultLogLevel">info</Property>
|
||||||
|
</Properties>
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||||
<PatternLayout>
|
<PatternLayout pattern="[%-5level] %d{HH:mm:ss.SSS} [%t] %c{2}.%M - %msg%n" />
|
||||||
<pattern>
|
|
||||||
[%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n
|
|
||||||
</pattern>>
|
|
||||||
</PatternLayout>
|
|
||||||
</Console>
|
</Console>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="info">
|
<Root level="info">
|
||||||
<AppenderRef ref="Console-Appender"/>
|
<AppenderRef ref="Console-Appender"/>
|
||||||
</Root>
|
</Root>
|
||||||
<Logger name="net.corda" level="info" additivity="false">
|
<Logger name="net.corda" level="${sys:defaultLogLevel}" additivity="false">
|
||||||
<AppenderRef ref="Console-Appender"/>
|
<AppenderRef ref="Console-Appender"/>
|
||||||
</Logger>
|
</Logger>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
|
@ -31,6 +31,7 @@ sourceSets {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
testCompile "junit:junit:$junit_version"
|
testCompile "junit:junit:$junit_version"
|
||||||
testCompile "commons-fileupload:commons-fileupload:1.3.2"
|
testCompile "commons-fileupload:commons-fileupload:1.3.2"
|
||||||
|
|
||||||
@ -56,6 +57,9 @@ dependencies {
|
|||||||
// AssertJ: for fluent assertions for testing
|
// AssertJ: for fluent assertions for testing
|
||||||
testCompile "org.assertj:assertj-core:${assertj_version}"
|
testCompile "org.assertj:assertj-core:${assertj_version}"
|
||||||
|
|
||||||
|
// TODO: Upgrade to junit-quickcheck 0.8, once it is released,
|
||||||
|
// because it depends on org.javassist:javassist instead
|
||||||
|
// of javassist:javassist.
|
||||||
compile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
compile "com.pholser:junit-quickcheck-core:$quickcheck_version"
|
||||||
compile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
|
compile "com.pholser:junit-quickcheck-generators:$quickcheck_version"
|
||||||
|
|
||||||
@ -63,7 +67,7 @@ dependencies {
|
|||||||
compile "com.google.guava:guava:$guava_version"
|
compile "com.google.guava:guava:$guava_version"
|
||||||
|
|
||||||
// RxJava: observable streams of events.
|
// RxJava: observable streams of events.
|
||||||
compile "io.reactivex:rxjava:1.2.4"
|
compile "io.reactivex:rxjava:$rxjava_version"
|
||||||
|
|
||||||
// Kryo: object graph serialization.
|
// Kryo: object graph serialization.
|
||||||
compile "com.esotericsoftware:kryo:4.0.0"
|
compile "com.esotericsoftware:kryo:4.0.0"
|
||||||
@ -88,4 +92,7 @@ dependencies {
|
|||||||
|
|
||||||
// RS API: Response type and codes for ApiUtils.
|
// RS API: Response type and codes for ApiUtils.
|
||||||
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
compile "javax.ws.rs:javax.ws.rs-api:2.0"
|
||||||
|
|
||||||
|
// Requery: SQL based query & persistence for Kotlin
|
||||||
|
compile "io.requery:requery-kotlin:$requery_version"
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import com.google.common.io.ByteStreams
|
|||||||
import com.google.common.util.concurrent.*
|
import com.google.common.util.concurrent.*
|
||||||
import kotlinx.support.jdk7.use
|
import kotlinx.support.jdk7.use
|
||||||
import net.corda.core.crypto.newSecureRandom
|
import net.corda.core.crypto.newSecureRandom
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Observer
|
import rx.Observer
|
||||||
@ -145,6 +146,7 @@ inline fun Path.write(createDirs: Boolean = false, vararg options: OpenOption =
|
|||||||
}
|
}
|
||||||
|
|
||||||
inline fun <R> Path.readLines(charset: Charset = UTF_8, block: (Stream<String>) -> R): R = Files.lines(this, charset).use(block)
|
inline fun <R> Path.readLines(charset: Charset = UTF_8, block: (Stream<String>) -> R): R = Files.lines(this, charset).use(block)
|
||||||
|
fun Path.readAllLines(charset: Charset = UTF_8): List<String> = Files.readAllLines(this, charset)
|
||||||
fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, vararg options: OpenOption): Path = Files.write(this, lines, charset, *options)
|
fun Path.writeLines(lines: Iterable<CharSequence>, charset: Charset = UTF_8, vararg options: OpenOption): Path = Files.write(this, lines, charset, *options)
|
||||||
|
|
||||||
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
|
fun InputStream.copyTo(target: Path, vararg options: CopyOption): Long = Files.copy(this, target, *options)
|
||||||
@ -203,7 +205,7 @@ inline fun elapsedTime(block: () -> Unit): Duration {
|
|||||||
val start = System.nanoTime()
|
val start = System.nanoTime()
|
||||||
block()
|
block()
|
||||||
val end = System.nanoTime()
|
val end = System.nanoTime()
|
||||||
return Duration.ofNanos(end-start)
|
return Duration.ofNanos(end - start)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
// TODO: Add inline back when a new Kotlin version is released and check if the java.lang.VerifyError
|
||||||
@ -257,6 +259,7 @@ class ThreadBox<out T>(val content: T, val lock: ReentrantLock = ReentrantLock()
|
|||||||
*
|
*
|
||||||
* We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization.
|
* We avoid the use of the word transient here to hopefully reduce confusion with the term in relation to (Java) serialization.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
abstract class RetryableException(message: String) : Exception(message)
|
abstract class RetryableException(message: String) : Exception(message)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -278,13 +281,16 @@ class TransientProperty<out T>(private val initializer: () -> T) {
|
|||||||
/**
|
/**
|
||||||
* Given a path to a zip file, extracts it to the given directory.
|
* Given a path to a zip file, extracts it to the given directory.
|
||||||
*/
|
*/
|
||||||
fun extractZipFile(zipFile: Path, toDirectory: Path) {
|
fun extractZipFile(zipFile: Path, toDirectory: Path) = extractZipFile(Files.newInputStream(zipFile), toDirectory)
|
||||||
val normalisedDirectory = toDirectory.normalize().createDirectories()
|
|
||||||
|
|
||||||
zipFile.read {
|
/**
|
||||||
val zip = ZipInputStream(BufferedInputStream(it))
|
* Given a zip file input stream, extracts it to the given directory.
|
||||||
|
*/
|
||||||
|
fun extractZipFile(inputStream: InputStream, toDirectory: Path) {
|
||||||
|
val normalisedDirectory = toDirectory.normalize().createDirectories()
|
||||||
|
ZipInputStream(BufferedInputStream(inputStream)).use {
|
||||||
while (true) {
|
while (true) {
|
||||||
val e = zip.nextEntry ?: break
|
val e = it.nextEntry ?: break
|
||||||
val outPath = (normalisedDirectory / e.name).normalize()
|
val outPath = (normalisedDirectory / e.name).normalize()
|
||||||
|
|
||||||
// Security checks: we should reject a zip that contains tricksy paths that try to escape toDirectory.
|
// Security checks: we should reject a zip that contains tricksy paths that try to escape toDirectory.
|
||||||
@ -295,9 +301,9 @@ fun extractZipFile(zipFile: Path, toDirectory: Path) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
outPath.write { out ->
|
outPath.write { out ->
|
||||||
ByteStreams.copy(zip, out)
|
ByteStreams.copy(it, out)
|
||||||
}
|
}
|
||||||
zip.closeEntry()
|
it.closeEntry()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -307,6 +313,7 @@ fun extractZipFile(zipFile: Path, toDirectory: Path) {
|
|||||||
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
|
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
|
||||||
|
|
||||||
/** Representation of an operation that may have thrown an error. */
|
/** Representation of an operation that may have thrown an error. */
|
||||||
|
@CordaSerializable
|
||||||
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
|
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
|
||||||
// The ErrorOr holds a value iff error == null
|
// The ErrorOr holds a value iff error == null
|
||||||
constructor(value: A) : this(value, null)
|
constructor(value: A) : this(value, null)
|
||||||
@ -391,15 +398,24 @@ private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<
|
|||||||
override fun onNext(value: T) {
|
override fun onNext(value: T) {
|
||||||
set(value)
|
set(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
override fun onError(e: Throwable) {
|
||||||
setException(e)
|
setException(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
||||||
subscription.unsubscribe()
|
subscription.unsubscribe()
|
||||||
return super.cancel(mayInterruptIfRunning)
|
return super.cancel(mayInterruptIfRunning)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCompleted() {}
|
override fun onCompleted() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||||
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
fun Iterable<BigDecimal>.sum(): BigDecimal = fold(BigDecimal.ZERO) { a, b -> a + b }
|
||||||
|
|
||||||
|
fun codePointsString(vararg codePoints: Int): String {
|
||||||
|
val builder = StringBuilder()
|
||||||
|
codePoints.forEach { builder.append(Character.toChars(it)) }
|
||||||
|
return builder.toString()
|
||||||
|
}
|
||||||
|
@ -12,7 +12,7 @@ val DUMMY_V2_PROGRAM_ID = DummyContractV2()
|
|||||||
* Dummy contract state for testing of the upgrade process.
|
* Dummy contract state for testing of the upgrade process.
|
||||||
*/
|
*/
|
||||||
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
class DummyContractV2 : UpgradedContract<DummyContract.State, DummyContractV2.State> {
|
||||||
override val legacyContract = DUMMY_PROGRAM_ID
|
override val legacyContract = DummyContract::class.java
|
||||||
|
|
||||||
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
|
data class State(val magicNumber: Int = 0, val owners: List<CompositeKey>) : ContractState {
|
||||||
override val contract = DUMMY_V2_PROGRAM_ID
|
override val contract = DUMMY_V2_PROGRAM_ID
|
||||||
|
@ -9,6 +9,7 @@ import com.fasterxml.jackson.databind.SerializerProvider
|
|||||||
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
||||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize
|
||||||
import com.google.common.annotations.VisibleForTesting
|
import com.google.common.annotations.VisibleForTesting
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import java.math.BigDecimal
|
import java.math.BigDecimal
|
||||||
import java.math.BigInteger
|
import java.math.BigInteger
|
||||||
import java.time.DayOfWeek
|
import java.time.DayOfWeek
|
||||||
@ -34,6 +35,7 @@ import java.util.*
|
|||||||
*
|
*
|
||||||
* @param T the type of the token, for example [Currency].
|
* @param T the type of the token, for example [Currency].
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@ -108,12 +110,14 @@ fun <T> Iterable<Amount<T>>.sumOrZero(currency: T) = if (iterator().hasNext()) s
|
|||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
/** A [FixOf] identifies the question side of a fix: what day, tenor and type of fix ("LIBOR", "EURIBOR" etc) */
|
/** 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)
|
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. */
|
/** 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
|
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
|
||||||
|
|
||||||
/** Represents a textual expression of e.g. a formula */
|
/** Represents a textual expression of e.g. a formula */
|
||||||
|
@CordaSerializable
|
||||||
@JsonDeserialize(using = ExpressionDeserializer::class)
|
@JsonDeserialize(using = ExpressionDeserializer::class)
|
||||||
@JsonSerialize(using = ExpressionSerializer::class)
|
@JsonSerialize(using = ExpressionSerializer::class)
|
||||||
data class Expression(val expr: String)
|
data class Expression(val expr: String)
|
||||||
@ -131,6 +135,7 @@ object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
||||||
|
@CordaSerializable
|
||||||
data class Tenor(val name: String) {
|
data class Tenor(val name: String) {
|
||||||
private val amount: Int
|
private val amount: Int
|
||||||
private val unit: TimeUnit
|
private val unit: TimeUnit
|
||||||
@ -166,6 +171,7 @@ data class Tenor(val name: String) {
|
|||||||
|
|
||||||
override fun toString(): String = name
|
override fun toString(): String = name
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
enum class TimeUnit(val code: String) {
|
enum class TimeUnit(val code: String) {
|
||||||
Day("D"), Week("W"), Month("M"), Year("Y")
|
Day("D"), Week("W"), Month("M"), Year("Y")
|
||||||
}
|
}
|
||||||
@ -175,6 +181,7 @@ data class Tenor(val name: String) {
|
|||||||
* Simple enum for returning accurals adjusted or unadjusted.
|
* Simple enum for returning accurals adjusted or unadjusted.
|
||||||
* We don't actually do anything with this yet though, so it's ignored for now.
|
* We don't actually do anything with this yet though, so it's ignored for now.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
enum class AccrualAdjustment {
|
enum class AccrualAdjustment {
|
||||||
Adjusted, Unadjusted
|
Adjusted, Unadjusted
|
||||||
}
|
}
|
||||||
@ -183,6 +190,7 @@ enum class AccrualAdjustment {
|
|||||||
* This is utilised in the [DateRollConvention] class to determine which way we should initially step when
|
* This is utilised in the [DateRollConvention] class to determine which way we should initially step when
|
||||||
* finding a business day.
|
* finding a business day.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -190,6 +198,7 @@ enum class DateRollDirection(val value: Long) { FORWARD(1), BACKWARD(-1) }
|
|||||||
* Depending on the accounting requirement, we can move forward until we get to a business day, or backwards.
|
* 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.
|
* There are some additional rules which are explained in the individual cases below.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
enum class DateRollConvention {
|
enum class DateRollConvention {
|
||||||
// direction() cannot be a val due to the throw in the Actual instance
|
// direction() cannot be a val due to the throw in the Actual instance
|
||||||
|
|
||||||
@ -235,6 +244,7 @@ enum class DateRollConvention {
|
|||||||
* Note that the first character cannot be a number (enum naming constraints), so we drop that
|
* Note that the first character cannot be a number (enum naming constraints), so we drop that
|
||||||
* in the toString lest some people get confused.
|
* in the toString lest some people get confused.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
enum class DayCountBasisDay {
|
enum class DayCountBasisDay {
|
||||||
// We have to prefix 30 etc with a letter due to enum naming constraints.
|
// We have to prefix 30 etc with a letter due to enum naming constraints.
|
||||||
D30,
|
D30,
|
||||||
@ -246,6 +256,7 @@ enum class DayCountBasisDay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** This forms the year part of the "Day Count Basis" used for interest calculation. */
|
/** This forms the year part of the "Day Count Basis" used for interest calculation. */
|
||||||
|
@CordaSerializable
|
||||||
enum class DayCountBasisYear {
|
enum class DayCountBasisYear {
|
||||||
// Ditto above comment for years.
|
// Ditto above comment for years.
|
||||||
Y360,
|
Y360,
|
||||||
@ -257,6 +268,7 @@ enum class DayCountBasisYear {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Whether the payment should be made before the due date, or after it. */
|
/** Whether the payment should be made before the due date, or after it. */
|
||||||
|
@CordaSerializable
|
||||||
enum class PaymentRule {
|
enum class PaymentRule {
|
||||||
InAdvance, InArrears,
|
InAdvance, InArrears,
|
||||||
}
|
}
|
||||||
@ -266,6 +278,7 @@ enum class PaymentRule {
|
|||||||
* that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc).
|
* 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.
|
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
|
||||||
|
@CordaSerializable
|
||||||
enum class Frequency(val annualCompoundCount: Int) {
|
enum class Frequency(val annualCompoundCount: Int) {
|
||||||
Annual(1) {
|
Annual(1) {
|
||||||
override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n)
|
override fun offset(d: LocalDate, n: Long) = d.plusYears(1 * n)
|
||||||
@ -304,7 +317,9 @@ fun LocalDate.isWorkingDay(accordingToCalendar: BusinessCalendar): Boolean = acc
|
|||||||
* typical feature of financial contracts, in which a business may not want a payment event to fall on a day when
|
* 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.
|
* no staff are around to handle problems.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
|
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
|
||||||
|
@CordaSerializable
|
||||||
class UnknownCalendar(name: String) : Exception("$name not found")
|
class UnknownCalendar(name: String) : Exception("$name not found")
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -434,6 +449,7 @@ fun calculateDaysBetween(startDate: LocalDate,
|
|||||||
* Enum for the types of netting that can be applied to state objects. Exact behaviour
|
* 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.
|
* for each type of netting is left to the contract to determine.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
enum class NetType {
|
enum class NetType {
|
||||||
/**
|
/**
|
||||||
* Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific),
|
* Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific),
|
||||||
@ -461,6 +477,7 @@ enum class NetType {
|
|||||||
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
|
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
|
||||||
* this commodity.
|
* this commodity.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Commodity(val commodityCode: String,
|
data class Commodity(val commodityCode: String,
|
||||||
val displayName: String,
|
val displayName: String,
|
||||||
val defaultFractionDigits: Int = 0) {
|
val defaultFractionDigits: Int = 0) {
|
||||||
@ -487,6 +504,7 @@ data class Commodity(val commodityCode: String,
|
|||||||
* So that the first time a state is issued this should be given a new UUID.
|
* 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.
|
* 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> {
|
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()
|
override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString()
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash
|
|||||||
import net.corda.core.flows.FlowLogicRef
|
import net.corda.core.flows.FlowLogicRef
|
||||||
import net.corda.core.flows.FlowLogicRefFactory
|
import net.corda.core.flows.FlowLogicRefFactory
|
||||||
import net.corda.core.node.services.ServiceType
|
import net.corda.core.node.services.ServiceType
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
@ -62,6 +63,7 @@ interface NettableState<N : BilateralNettableState<N>, T : Any> : BilateralNetta
|
|||||||
* notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states
|
* notary is responsible for ensuring there is no "double spending" by only signing a transaction if the input states
|
||||||
* are all free.
|
* are all free.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
interface ContractState {
|
interface ContractState {
|
||||||
/**
|
/**
|
||||||
* An instance of the contract class that will verify this state.
|
* An instance of the contract class that will verify this state.
|
||||||
@ -121,6 +123,7 @@ interface ContractState {
|
|||||||
* A wrapper for [ContractState] containing additional platform-level state information.
|
* 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.
|
* This is the definitive state that is stored on the ledger and used in transaction outputs.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
data class TransactionState<out T : ContractState> @JvmOverloads constructor(
|
||||||
/** The custom contract state */
|
/** The custom contract state */
|
||||||
val data: T,
|
val data: T,
|
||||||
@ -169,6 +172,7 @@ interface IssuanceDefinition
|
|||||||
*
|
*
|
||||||
* @param P the type of product underlying the definition, for example [Currency].
|
* @param P the type of product underlying the definition, for example [Currency].
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
|
data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
|
||||||
override fun toString() = "$product issued by $issuer"
|
override fun toString() = "$product issued by $issuer"
|
||||||
}
|
}
|
||||||
@ -239,6 +243,7 @@ interface LinearState : ContractState {
|
|||||||
/**
|
/**
|
||||||
* Standard clause to verify the LinearState safety properties.
|
* Standard clause to verify the LinearState safety properties.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class ClauseVerifier<S : LinearState, C : CommandData>() : Clause<S, C, Unit>() {
|
class ClauseVerifier<S : LinearState, C : CommandData>() : Clause<S, C, Unit>() {
|
||||||
override fun verify(tx: TransactionForContract,
|
override fun verify(tx: TransactionForContract,
|
||||||
inputs: List<S>,
|
inputs: List<S>,
|
||||||
@ -290,7 +295,7 @@ interface DealState : LinearState {
|
|||||||
* separate process exchange certificates to ascertain identities. Thus decoupling identities from
|
* separate process exchange certificates to ascertain identities. Thus decoupling identities from
|
||||||
* [ContractState]s.
|
* [ContractState]s.
|
||||||
* */
|
* */
|
||||||
val parties: List<Party>
|
val parties: List<AnonymousParty>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general
|
* Generate a partial transaction representing an agreement (command) to this deal, allowing a general
|
||||||
@ -334,11 +339,13 @@ fun ContractState.hash(): SecureHash = SecureHash.sha256(serialize().bytes)
|
|||||||
* A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which
|
* A stateref is a pointer (reference) to a state, this is an equivalent of an "outpoint" in Bitcoin. It records which
|
||||||
* transaction defined the state and where in that transaction it was.
|
* transaction defined the state and where in that transaction it was.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class StateRef(val txhash: SecureHash, val index: Int) {
|
data class StateRef(val txhash: SecureHash, val index: Int) {
|
||||||
override fun toString() = "$txhash($index)"
|
override fun toString() = "$txhash($index)"
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */
|
/** A StateAndRef is simply a (state, ref) pair. For instance, a vault (which holds available assets) contains these. */
|
||||||
|
@CordaSerializable
|
||||||
data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, val ref: StateRef)
|
data class StateAndRef<out T : ContractState>(val state: TransactionState<T>, val ref: StateRef)
|
||||||
|
|
||||||
/** Filters a list of [StateAndRef] objects according to the type of the states */
|
/** Filters a list of [StateAndRef] objects according to the type of the states */
|
||||||
@ -350,11 +357,14 @@ inline fun <reified T : ContractState> Iterable<StateAndRef<ContractState>>.filt
|
|||||||
* Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
|
* Reference to something being stored or issued by a party e.g. in a vault or (more likely) on their normal
|
||||||
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party.
|
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
|
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
|
||||||
|
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
|
||||||
override fun toString() = "${party}$reference"
|
override fun toString() = "${party}$reference"
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Marker interface for classes that represent commands */
|
/** Marker interface for classes that represent commands */
|
||||||
|
@CordaSerializable
|
||||||
interface CommandData
|
interface CommandData
|
||||||
|
|
||||||
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
|
/** Commands that inherit from this are intended to have no data items: it's only their presence that matters. */
|
||||||
@ -364,6 +374,7 @@ abstract class TypeOnlyCommandData : CommandData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
/** Command data/content plus pubkey pair: the signature is stored at the end of the serialized bytes */
|
||||||
|
@CordaSerializable
|
||||||
data class Command(val value: CommandData, val signers: List<CompositeKey>) {
|
data class Command(val value: CommandData, val signers: List<CompositeKey>) {
|
||||||
init {
|
init {
|
||||||
require(signers.isNotEmpty())
|
require(signers.isNotEmpty())
|
||||||
@ -398,9 +409,10 @@ interface NetCommand : CommandData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
/** Indicates that this transaction replaces the inputs contract state to another contract state */
|
||||||
data class UpgradeCommand(val upgradedContractClass: Class<UpgradedContract<*, *>>) : CommandData
|
data class UpgradeCommand(val upgradedContractClass: Class<out UpgradedContract<*, *>>) : CommandData
|
||||||
|
|
||||||
/** Wraps an object that was signed by a public key, which may be a well known/recognised institutional key. */
|
/** 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>(
|
data class AuthenticatedObject<out T : Any>(
|
||||||
val signers: List<CompositeKey>,
|
val signers: List<CompositeKey>,
|
||||||
/** If any public keys were recognised, the looked up institutions are available here */
|
/** If any public keys were recognised, the looked up institutions are available here */
|
||||||
@ -412,6 +424,7 @@ data class AuthenticatedObject<out T : Any>(
|
|||||||
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
|
* If present in a transaction, contains a time that was verified by the uniqueness service. The true time must be
|
||||||
* between (after, before).
|
* between (after, before).
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Timestamp(val after: Instant?, val before: Instant?) {
|
data class Timestamp(val after: Instant?, val before: Instant?) {
|
||||||
init {
|
init {
|
||||||
if (after == null && before == null)
|
if (after == null && before == null)
|
||||||
@ -430,7 +443,10 @@ data class Timestamp(val after: Instant?, val before: Instant?) {
|
|||||||
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the
|
* 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
|
* 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.
|
* timestamp 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.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
interface Contract {
|
interface Contract {
|
||||||
/**
|
/**
|
||||||
* Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense.
|
* Takes an object that represents a state transition, and ensures the inputs/outputs/commands make sense.
|
||||||
@ -456,7 +472,7 @@ interface Contract {
|
|||||||
* @param NewState the upgraded contract state.
|
* @param NewState the upgraded contract state.
|
||||||
*/
|
*/
|
||||||
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
|
interface UpgradedContract<in OldState : ContractState, out NewState : ContractState> : Contract {
|
||||||
val legacyContract: Contract
|
val legacyContract: Class<out Contract>
|
||||||
/**
|
/**
|
||||||
* Upgrade contract's state object to a new state object.
|
* Upgrade contract's state object to a new state object.
|
||||||
*
|
*
|
||||||
|
@ -2,10 +2,12 @@ package net.corda.core.contracts
|
|||||||
|
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
|
|
||||||
/** Defines transaction build & validation logic for a specific transaction type */
|
/** Defines transaction build & validation logic for a specific transaction type */
|
||||||
|
@CordaSerializable
|
||||||
sealed class TransactionType {
|
sealed class TransactionType {
|
||||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||||
override fun hashCode() = javaClass.name.hashCode()
|
override fun hashCode() = javaClass.name.hashCode()
|
||||||
|
@ -4,6 +4,7 @@ import net.corda.core.crypto.CompositeKey
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -94,6 +95,7 @@ class AttachmentResolutionException(val hash : SecureHash) : FlowException() {
|
|||||||
override fun toString(): String = "Attachment resolution failure for $hash"
|
override fun toString(): String = "Attachment resolution failure for $hash"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||||
|
|
||||||
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) {
|
sealed class TransactionVerificationException(val tx: LedgerTransaction, cause: Throwable?) : FlowException(cause) {
|
||||||
@ -116,6 +118,8 @@ sealed class TransactionVerificationException(val tx: LedgerTransaction, cause:
|
|||||||
class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) {
|
class TransactionMissingEncumbranceException(tx: LedgerTransaction, val missing: Int, val inOut: Direction) : TransactionVerificationException(tx, null) {
|
||||||
override val message: String get() = "Missing required encumbrance $missing in $inOut"
|
override val message: String get() = "Missing required encumbrance $missing in $inOut"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
enum class Direction {
|
enum class Direction {
|
||||||
INPUT,
|
INPUT,
|
||||||
OUTPUT
|
OUTPUT
|
||||||
|
25
core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt
Normal file
25
core/src/main/kotlin/net/corda/core/crypto/AbstractParty.kt
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.contracts.PartyAndReference
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
|
import java.security.PublicKey
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An [AbstractParty] contains the common elements of [Party] and [AnonymousParty], specifically the owning key of
|
||||||
|
* the party. In most cases [Party] or [AnonymousParty] should be used, depending on use-case.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
abstract class AbstractParty(val owningKey: CompositeKey) {
|
||||||
|
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
||||||
|
constructor(owningKey: PublicKey) : this(owningKey.composite)
|
||||||
|
|
||||||
|
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
||||||
|
override fun equals(other: Any?): Boolean = other is AbstractParty && this.owningKey == other.owningKey
|
||||||
|
override fun hashCode(): Int = owningKey.hashCode()
|
||||||
|
abstract fun toAnonymous() : AnonymousParty
|
||||||
|
abstract fun nameOrNull() : String?
|
||||||
|
|
||||||
|
abstract fun ref(bytes: OpaqueBytes): PartyAndReference
|
||||||
|
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
|
||||||
|
}
|
@ -8,17 +8,16 @@ import java.security.PublicKey
|
|||||||
* The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private
|
* The [AnonymousParty] class contains enough information to uniquely identify a [Party] while excluding private
|
||||||
* information such as name. It is intended to represent a party on the distributed ledger.
|
* information such as name. It is intended to represent a party on the distributed ledger.
|
||||||
*/
|
*/
|
||||||
open class AnonymousParty(val owningKey: CompositeKey) {
|
class AnonymousParty(owningKey: CompositeKey) : AbstractParty(owningKey) {
|
||||||
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
||||||
constructor(owningKey: PublicKey) : this(owningKey.composite)
|
constructor(owningKey: PublicKey) : this(owningKey.composite)
|
||||||
|
|
||||||
/** Anonymised parties do not include any detail apart from owning key, so equality is dependent solely on the key */
|
|
||||||
override fun equals(other: Any?): Boolean = other is AnonymousParty && this.owningKey == other.owningKey
|
|
||||||
override fun hashCode(): Int = owningKey.hashCode()
|
|
||||||
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
|
// Use the key as the bulk of the toString(), but include a human readable identifier as well, so that [Party]
|
||||||
// can put in the key and actual name
|
// can put in the key and actual name
|
||||||
override fun toString() = "${owningKey.toBase58String()} <Anonymous>"
|
override fun toString() = "${owningKey.toBase58String()} <Anonymous>"
|
||||||
|
|
||||||
fun ref(bytes: OpaqueBytes) = PartyAndReference(this, bytes)
|
override fun nameOrNull(): String? = null
|
||||||
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
|
|
||||||
|
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this, bytes)
|
||||||
|
override fun toAnonymous() = this
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ package net.corda.core.crypto
|
|||||||
|
|
||||||
import net.corda.core.crypto.CompositeKey.Leaf
|
import net.corda.core.crypto.CompositeKey.Leaf
|
||||||
import net.corda.core.crypto.CompositeKey.Node
|
import net.corda.core.crypto.CompositeKey.Node
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
@ -19,6 +20,7 @@ import java.security.PublicKey
|
|||||||
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
|
* Using these constructs we can express e.g. 1 of N (OR) or N of N (AND) signature requirements. By nesting we can
|
||||||
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
|
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
sealed class CompositeKey {
|
sealed class CompositeKey {
|
||||||
/** Checks whether [keys] match a sufficient amount of leaf nodes */
|
/** Checks whether [keys] match a sufficient amount of leaf nodes */
|
||||||
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
|
abstract fun isFulfilledBy(keys: Iterable<PublicKey>): Boolean
|
||||||
|
421
core/src/main/kotlin/net/corda/core/crypto/Crypto.kt
Normal file
421
core/src/main/kotlin/net/corda/core/crypto/Crypto.kt
Normal file
@ -0,0 +1,421 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAKey
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||||
|
import org.bouncycastle.jce.ECNamedCurveTable
|
||||||
|
import org.bouncycastle.jce.interfaces.ECKey
|
||||||
|
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
|
||||||
|
import java.security.*
|
||||||
|
import java.security.spec.InvalidKeySpecException
|
||||||
|
import java.security.spec.PKCS8EncodedKeySpec
|
||||||
|
import java.security.spec.X509EncodedKeySpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This object controls and provides the available and supported signature schemes for Corda.
|
||||||
|
* Any implemented [SignatureScheme] should be strictly defined here.
|
||||||
|
* However, only the schemes returned by {@link #listSupportedSignatureSchemes()} are supported.
|
||||||
|
* Note that Corda currently supports the following signature schemes by their code names:
|
||||||
|
* <p><ul>
|
||||||
|
* <li>RSA_SHA256 (RSA using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function).
|
||||||
|
* <li>ECDSA_SECP256K1_SHA256 (ECDSA using the secp256k1 Koblitz curve and SHA256 as hash algorithm).
|
||||||
|
* <li>ECDSA_SECP256R1_SHA256 (ECDSA using the secp256r1 (NIST P-256) curve and SHA256 as hash algorithm).
|
||||||
|
* <li>EDDSA_ED25519_SHA512 (EdDSA using the ed255519 twisted Edwards curve and SHA512 as hash algorithm).
|
||||||
|
* <li>SPHINCS256_SHA512 (SPHINCS-256 hash-based signature scheme using SHA512 as hash algorithm).
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
object Crypto {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function.
|
||||||
|
* Note: Recommended key size >= 3072 bits.
|
||||||
|
*/
|
||||||
|
private val RSA_SHA256 = SignatureScheme(
|
||||||
|
1,
|
||||||
|
"RSA_SHA256",
|
||||||
|
"RSA",
|
||||||
|
Signature.getInstance("SHA256WITHRSAANDMGF1", "BC"),
|
||||||
|
KeyFactory.getInstance("RSA", "BC"),
|
||||||
|
KeyPairGenerator.getInstance("RSA", "BC"),
|
||||||
|
null,
|
||||||
|
3072,
|
||||||
|
"RSA_SHA256 signature scheme using SHA256 as hash algorithm and MGF1 (with SHA256) as mask generation function."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** ECDSA signature scheme using the secp256k1 Koblitz curve. */
|
||||||
|
private val ECDSA_SECP256K1_SHA256 = SignatureScheme(
|
||||||
|
2,
|
||||||
|
"ECDSA_SECP256K1_SHA256",
|
||||||
|
"ECDSA",
|
||||||
|
Signature.getInstance("SHA256withECDSA", "BC"),
|
||||||
|
KeyFactory.getInstance("ECDSA", "BC"),
|
||||||
|
KeyPairGenerator.getInstance("ECDSA", "BC"),
|
||||||
|
ECNamedCurveTable.getParameterSpec("secp256k1"),
|
||||||
|
256,
|
||||||
|
"ECDSA signature scheme using the secp256k1 Koblitz curve."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** ECDSA signature scheme using the secp256r1 (NIST P-256) curve. */
|
||||||
|
private val ECDSA_SECP256R1_SHA256 = SignatureScheme(
|
||||||
|
3,
|
||||||
|
"ECDSA_SECP256R1_SHA256",
|
||||||
|
"ECDSA",
|
||||||
|
Signature.getInstance("SHA256withECDSA", "BC"),
|
||||||
|
KeyFactory.getInstance("ECDSA", "BC"),
|
||||||
|
KeyPairGenerator.getInstance("ECDSA", "BC"),
|
||||||
|
ECNamedCurveTable.getParameterSpec("secp256r1"),
|
||||||
|
256,
|
||||||
|
"ECDSA signature scheme using the secp256r1 (NIST P-256) curve."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** EdDSA signature scheme using the ed255519 twisted Edwards curve. */
|
||||||
|
private val EDDSA_ED25519_SHA512 = SignatureScheme(
|
||||||
|
4,
|
||||||
|
"EDDSA_ED25519_SHA512",
|
||||||
|
"EdDSA",
|
||||||
|
EdDSAEngine(),
|
||||||
|
EdDSAKeyFactory(),
|
||||||
|
net.i2p.crypto.eddsa.KeyPairGenerator(), // EdDSA engine uses a custom KeyPairGenerator Vs BouncyCastle.
|
||||||
|
EdDSANamedCurveTable.getByName("ed25519-sha-512"),
|
||||||
|
256,
|
||||||
|
"EdDSA signature scheme using the ed25519 twisted Edwards curve."
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers
|
||||||
|
* at the cost of larger key sizes and loss of compatibility.
|
||||||
|
*/
|
||||||
|
private val SPHINCS256_SHA256 = SignatureScheme(
|
||||||
|
5,
|
||||||
|
"SPHINCS-256_SHA512",
|
||||||
|
"SPHINCS-256",
|
||||||
|
Signature.getInstance("SHA512WITHSPHINCS256", "BCPQC"),
|
||||||
|
KeyFactory.getInstance("SPHINCS256", "BCPQC"),
|
||||||
|
KeyPairGenerator.getInstance("SPHINCS256", "BCPQC"),
|
||||||
|
SPHINCS256KeyGenParameterSpec(SPHINCS256KeyGenParameterSpec.SHA512_256),
|
||||||
|
256,
|
||||||
|
"SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers " +
|
||||||
|
"at the cost of larger key sizes and loss of compatibility."
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
|
||||||
|
private val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported digital signature schemes.
|
||||||
|
* Note: Only the schemes added in this map will be supported (see [Crypto]).
|
||||||
|
* Do not forget to add the DEFAULT_SIGNATURE_SCHEME as well.
|
||||||
|
*/
|
||||||
|
private val supportedSignatureSchemes = mapOf(
|
||||||
|
RSA_SHA256.schemeCodeName to RSA_SHA256,
|
||||||
|
ECDSA_SECP256K1_SHA256.schemeCodeName to ECDSA_SECP256K1_SHA256,
|
||||||
|
ECDSA_SECP256R1_SHA256.schemeCodeName to ECDSA_SECP256R1_SHA256,
|
||||||
|
EDDSA_ED25519_SHA512.schemeCodeName to EDDSA_ED25519_SHA512,
|
||||||
|
SPHINCS256_SHA256.schemeCodeName to SPHINCS256_SHA256
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory pattern to retrieve the corresponding [SignatureScheme] based on the type of the [String] input.
|
||||||
|
* This function is usually called by key generators and verify signature functions.
|
||||||
|
* In case the input is not a key in the supportedSignatureSchemes map, null will be returned.
|
||||||
|
* @param schemeCodeName a [String] that should match a supported signature scheme code name (e.g. ECDSA_SECP256K1_SHA256), see [Crypto].
|
||||||
|
* @return a currently supported SignatureScheme.
|
||||||
|
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||||
|
*/
|
||||||
|
private fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for metadata schemeCodeName: ${schemeCodeName}")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the corresponding [SignatureScheme] based on the type of the input [KeyPair].
|
||||||
|
* Note that only the Corda platform standard schemes are supported (see [Crypto]).
|
||||||
|
* This function is usually called when requiring to sign signatures.
|
||||||
|
* @param keyPair a cryptographic [KeyPair].
|
||||||
|
* @return a currently supported SignatureScheme or null.
|
||||||
|
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||||
|
*/
|
||||||
|
private fun findSignatureScheme(keyPair: KeyPair): SignatureScheme = findSignatureScheme(keyPair.private)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the corresponding [SignatureScheme] based on the type of the input [Key].
|
||||||
|
* This function is usually called when requiring to verify signatures and the signing schemes must be defined.
|
||||||
|
* Note that only the Corda platform standard schemes are supported (see [Crypto]).
|
||||||
|
* Note that we always need to add an additional if-else statement when there are signature schemes
|
||||||
|
* with the same algorithmName, but with different parameters (e.g. now there are two ECDSA schemes, each using its own curve).
|
||||||
|
* @param key either private or public.
|
||||||
|
* @return a currently supported SignatureScheme.
|
||||||
|
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||||
|
*/
|
||||||
|
private fun findSignatureScheme(key: Key): SignatureScheme {
|
||||||
|
for (sig in supportedSignatureSchemes.values) {
|
||||||
|
val algorithm = key.algorithm
|
||||||
|
if (algorithm == sig.algorithmName) {
|
||||||
|
// If more than one ECDSA schemes are supported, we should distinguish between them by checking their curve parameters.
|
||||||
|
// TODO: change 'continue' to 'break' if only one EdDSA curve will be used.
|
||||||
|
if (algorithm == "EdDSA") {
|
||||||
|
if ((key as EdDSAKey).params == sig.algSpec) {
|
||||||
|
return sig
|
||||||
|
} else continue
|
||||||
|
} else if (algorithm == "ECDSA") {
|
||||||
|
if ((key as ECKey).parameters == sig.algSpec) {
|
||||||
|
return sig
|
||||||
|
} else continue
|
||||||
|
} else return sig // it's either RSA_SHA256 or SPHINCS-256.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unsupported key/algorithm for the private key: ${key.encoded.toBase58()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the corresponding signature scheme code name based on the type of the input [Key].
|
||||||
|
* See [Crypto] for the supported scheme code names.
|
||||||
|
* @param key either private or public.
|
||||||
|
* @return signatureSchemeCodeName for a [Key].
|
||||||
|
* @throws IllegalArgumentException if the requested key type is not supported.
|
||||||
|
*/
|
||||||
|
fun findSignatureSchemeCodeName(key: Key): String = findSignatureScheme(key).schemeCodeName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PKCS8 encoded key to its [PrivateKey] object.
|
||||||
|
* @param encodedKey a PKCS8 encoded private key.
|
||||||
|
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||||
|
* is inappropriate for this key factory to produce a private key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
|
||||||
|
for (sig in supportedSignatureSchemes.values) {
|
||||||
|
try {
|
||||||
|
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||||
|
} catch (ikse: InvalidKeySpecException) {
|
||||||
|
// ignore it - only used to bypass the scheme that causes an exception.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a PKCS8 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||||
|
* This will be used by Kryo deserialisation.
|
||||||
|
* @param encodedKey a PKCS8 encoded private key.
|
||||||
|
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||||
|
* is inappropriate for this key factory to produce a private key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||||
|
fun decodePrivateKey(encodedKey: ByteArray, schemeCodeName: String): PrivateKey {
|
||||||
|
val sig = findSignatureScheme(schemeCodeName)
|
||||||
|
try {
|
||||||
|
return sig.keyFactory.generatePrivate(PKCS8EncodedKeySpec(encodedKey))
|
||||||
|
} catch (ikse: InvalidKeySpecException) {
|
||||||
|
throw InvalidKeySpecException("This private key cannot be decoded, please ensure it is PKCS8 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an X509 encoded key to its [PublicKey] object.
|
||||||
|
* @param encodedKey an X509 encoded public key.
|
||||||
|
* @throws UnsupportedSchemeException on not supported scheme.
|
||||||
|
* @throws IllegalArgumentException on not supported scheme or if the given key specification
|
||||||
|
* is inappropriate for this key factory to produce a private key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
|
||||||
|
for (sig in supportedSignatureSchemes.values) {
|
||||||
|
try {
|
||||||
|
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||||
|
} catch (ikse: InvalidKeySpecException) {
|
||||||
|
// ignore it - only used to bypass the scheme that causes an exception.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode an X509 encoded key to its [PrivateKey] object based on the input scheme code name.
|
||||||
|
* This will be used by Kryo deserialisation.
|
||||||
|
* @param encodedKey an X509 encoded public key.
|
||||||
|
* @param schemeCodeName a [String] that should match a key in supportedSignatureSchemes map (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @throws IllegalArgumentException if the requested scheme is not supported
|
||||||
|
* @throws InvalidKeySpecException if the given key specification
|
||||||
|
* is inappropriate for this key factory to produce a public key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||||
|
fun decodePublicKey(encodedKey: ByteArray, schemeCodeName: String): PublicKey {
|
||||||
|
val sig = findSignatureScheme(schemeCodeName)
|
||||||
|
try {
|
||||||
|
return sig.keyFactory.generatePublic(X509EncodedKeySpec(encodedKey))
|
||||||
|
} catch (ikse: InvalidKeySpecException) {
|
||||||
|
throw throw InvalidKeySpecException("This public key cannot be decoded, please ensure it is X509 encoded and that it corresponds to the input scheme's code name.", ikse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to simplify the act of generating keys.
|
||||||
|
* Normally, we don't expect other errors here, assuming that key generation parameters for every supported signature scheme have been unit-tested.
|
||||||
|
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @return a KeyPair for the requested scheme.
|
||||||
|
* @throws IllegalArgumentException if the requested signature scheme is not supported.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class)
|
||||||
|
fun generateKeyPair(schemeCodeName: String): KeyPair = findSignatureScheme(schemeCodeName).keyPairGenerator.generateKeyPair()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a KeyPair using the default signature scheme.
|
||||||
|
* @return a new KeyPair.
|
||||||
|
*/
|
||||||
|
fun generateKeyPair(): KeyPair = DEFAULT_SIGNATURE_SCHEME.keyPairGenerator.generateKeyPair()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic way to sign [ByteArray] data with a [PrivateKey]. Strategy on on identifying the actual signing scheme is based
|
||||||
|
* on the [PrivateKey] type, but if the schemeCodeName is known, then better use doSign(signatureScheme: String, privateKey: PrivateKey, clearData: ByteArray).
|
||||||
|
* @param privateKey the signer's [PrivateKey].
|
||||||
|
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||||
|
* @return the digital signature (in [ByteArray]) on the input message.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
fun doSign(privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(privateKey).sig, privateKey, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known schemeCodeName [String].
|
||||||
|
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @param privateKey the signer's [PrivateKey].
|
||||||
|
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||||
|
* @return the digital signature (in [ByteArray]) on the input message.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported.
|
||||||
|
* @throws InvalidKeyException if the private key is invalid.
|
||||||
|
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
fun doSign(schemeCodeName: String, privateKey: PrivateKey, clearData: ByteArray) = doSign(findSignatureScheme(schemeCodeName).sig, privateKey, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic way to sign [ByteArray] data with a [PrivateKey] and a known [Signature].
|
||||||
|
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
|
||||||
|
* @param privateKey the signer's [PrivateKey].
|
||||||
|
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||||
|
* @return the digital signature (in [ByteArray]) on the input message.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
private fun doSign(signature: Signature, privateKey: PrivateKey, clearData: ByteArray): ByteArray {
|
||||||
|
if (clearData.isEmpty()) throw Exception("Signing of an empty array is not permitted!")
|
||||||
|
signature.initSign(privateKey)
|
||||||
|
signature.update(clearData)
|
||||||
|
return signature.sign()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic way to sign [MetaData] objects with a [PrivateKey].
|
||||||
|
* [MetaData] is a wrapper over the transaction's Merkle root in order to attach extra information, such as a timestamp or partial and blind signature indicators.
|
||||||
|
* @param privateKey the signer's [PrivateKey].
|
||||||
|
* @param metaData a [MetaData] object that adds extra information to a transaction.
|
||||||
|
* @return a [TransactionSignature] object than contains the output of a successful signing and the metaData.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or
|
||||||
|
* if metaData.schemeCodeName is not aligned with key type.
|
||||||
|
* @throws InvalidKeyException if the private key is invalid.
|
||||||
|
* @throws SignatureException if signing is not possible due to malformed data or private key.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
fun doSign(privateKey: PrivateKey, metaData: MetaData): TransactionSignature {
|
||||||
|
val sigKey: SignatureScheme = findSignatureScheme(privateKey)
|
||||||
|
val sigMetaData: SignatureScheme = findSignatureScheme(metaData.schemeCodeName)
|
||||||
|
if (sigKey != sigMetaData) throw IllegalArgumentException("Metadata schemeCodeName: ${metaData.schemeCodeName} is not aligned with the key type.")
|
||||||
|
val signatureData = doSign(sigKey.schemeCodeName, privateKey, metaData.bytes())
|
||||||
|
return TransactionSignature(signatureData, metaData)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to simplify the act of verifying a digital signature.
|
||||||
|
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||||
|
* @param publicKey the signer's [PublicKey].
|
||||||
|
* @param signatureData the signatureData on a message.
|
||||||
|
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||||
|
* @return true if verification passes or throws an exception if verification fails.
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(schemeCodeName).sig, publicKey, signatureData, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to simplify the act of verifying a digital signature by identifying the signature scheme used from the input public key's type.
|
||||||
|
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||||
|
* Strategy on identifying the actual signing scheme is based on the [PublicKey] type, but if the schemeCodeName is known,
|
||||||
|
* then better use doVerify(schemeCodeName: String, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray).
|
||||||
|
* @param publicKey the signer's [PublicKey].
|
||||||
|
* @param signatureData the signatureData on a message.
|
||||||
|
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||||
|
* @return true if verification passes or throws an exception if verification fails.
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun doVerify(publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray) = doVerify(findSignatureScheme(publicKey).sig, publicKey, signatureData, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to verify a digital signature.
|
||||||
|
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||||
|
* @param signature a [Signature] object, retrieved from supported signature schemes, see [Crypto].
|
||||||
|
* @param publicKey the signer's [PublicKey].
|
||||||
|
* @param signatureData the signatureData on a message.
|
||||||
|
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||||
|
* @return true if verification passes or throws an exception if verification fails.
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||||
|
* @throws IllegalArgumentException if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
private fun doVerify(signature: Signature, publicKey: PublicKey, signatureData: ByteArray, clearData: ByteArray): Boolean {
|
||||||
|
if (signatureData.isEmpty()) throw IllegalArgumentException("Signature data is empty!")
|
||||||
|
if (clearData.isEmpty()) throw IllegalArgumentException("Clear data is empty, nothing to verify!")
|
||||||
|
signature.initVerify(publicKey)
|
||||||
|
signature.update(clearData)
|
||||||
|
val verificationResult = signature.verify(signatureData)
|
||||||
|
if (verificationResult) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
throw SignatureException("Signature Verification failed!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to simplify the act of verifying a [TransactionSignature].
|
||||||
|
* It returns true if it succeeds, but it always throws an exception if verification fails.
|
||||||
|
* @param publicKey the signer's [PublicKey].
|
||||||
|
* @param transactionSignature the signatureData on a message.
|
||||||
|
* @return true if verification passes or throws an exception if verification fails.
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData scheme is unable to process the input data provided, if the verification is not possible.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun doVerify(publicKey: PublicKey, transactionSignature: TransactionSignature): Boolean {
|
||||||
|
if (publicKey != transactionSignature.metaData.publicKey) IllegalArgumentException("MetaData's publicKey: ${transactionSignature.metaData.publicKey.encoded.toBase58()} does not match the input clearData: ${publicKey.encoded.toBase58()}")
|
||||||
|
return Crypto.doVerify(publicKey, transactionSignature.signatureData, transactionSignature.metaData.bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the requested signature scheme is supported by the system.
|
||||||
|
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @return true if the signature scheme is supported.
|
||||||
|
*/
|
||||||
|
fun isSupportedSignatureScheme(schemeCodeName: String): Boolean = schemeCodeName in supportedSignatureSchemes
|
||||||
|
|
||||||
|
/** @return the default signature scheme's code name. */
|
||||||
|
fun getDefaultSignatureSchemeCodeName(): String = DEFAULT_SIGNATURE_SCHEME.schemeCodeName
|
||||||
|
|
||||||
|
/** @return a [List] of Strings with the scheme code names defined in [SignatureScheme] for all of our supported signature schemes, see [Crypto]. */
|
||||||
|
fun listSupportedSignatureSchemes(): List<String> = supportedSignatureSchemes.keys.toList()
|
||||||
|
}
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
@ -21,11 +22,8 @@ fun newSecureRandom(): SecureRandom {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** A wrapper around a digital signature. */
|
||||||
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
@CordaSerializable
|
||||||
* signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when
|
|
||||||
* building partially signed transactions.
|
|
||||||
*/
|
|
||||||
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
||||||
/** A digital signature that identifies who the public key is owned by. */
|
/** A digital signature that identifies who the public key is owned by. */
|
||||||
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
|
open class WithKey(val by: PublicKey, bits: ByteArray) : DigitalSignature(bits) {
|
||||||
@ -37,6 +35,7 @@ open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
|||||||
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey.singleKey, bits)
|
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey.singleKey, bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||||
override fun getAlgorithm() = "NULL"
|
override fun getAlgorithm() = "NULL"
|
||||||
override fun getEncoded() = byteArrayOf(0)
|
override fun getEncoded() = byteArrayOf(0)
|
||||||
@ -48,6 +47,7 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
|||||||
val NullCompositeKey = NullPublicKey.composite
|
val NullCompositeKey = NullPublicKey.composite
|
||||||
|
|
||||||
// TODO: Clean up this duplication between Null and Dummy public key
|
// TODO: Clean up this duplication between Null and Dummy public key
|
||||||
|
@CordaSerializable
|
||||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||||
override fun getAlgorithm() = "DUMMY"
|
override fun getAlgorithm() = "DUMMY"
|
||||||
override fun getEncoded() = s.toByteArray()
|
override fun getEncoded() = s.toByteArray()
|
||||||
@ -59,6 +59,7 @@ class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
/** A signature with a key and value of zero. Useful when you want a signature object that you know won't ever be used. */
|
||||||
|
@CordaSerializable
|
||||||
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
||||||
|
|
||||||
/** Utility to simplify the act of signing a byte array */
|
/** Utility to simplify the act of signing a byte array */
|
||||||
|
117
core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt
Normal file
117
core/src/main/kotlin/net/corda/core/crypto/CryptoUtils.kt
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import java.security.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for signing.
|
||||||
|
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||||
|
* @return the digital signature (in [ByteArray]) on the input message.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
fun PrivateKey.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for signing.
|
||||||
|
* @param metaDataFull tha attached MetaData object.
|
||||||
|
* @return a [DSWithMetaDataFull] 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.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun PrivateKey.sign(metaData: MetaData): TransactionSignature = Crypto.doSign(this, metaData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to sign with a key pair.
|
||||||
|
* @param clearData the data/message to be signed in [ByteArray] form (usually the Merkle root).
|
||||||
|
* @return the digital signature (in [ByteArray]) on the input message.
|
||||||
|
* @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.
|
||||||
|
*/
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
|
||||||
|
fun KeyPair.sign(clearData: ByteArray): ByteArray = Crypto.doSign(this.private, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to verify a signature.
|
||||||
|
* @param signatureData the signature on a message.
|
||||||
|
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun PublicKey.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this, signatureData, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to verify a metadata attached signature. It is noted that the transactionSignature contains
|
||||||
|
* signatureData and a [MetaData] object that contains the signer's public key and the transaction's Merkle root.
|
||||||
|
* @param transactionSignature a [TransactionSignature] object that .
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun PublicKey.verify(transactionSignature: TransactionSignature): Boolean {
|
||||||
|
return Crypto.doVerify(this, transactionSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function for the signers to verify their own signature.
|
||||||
|
* @param signature the signature on a message.
|
||||||
|
* @param clearData the clear data/message that was signed (usually the Merkle root).
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun KeyPair.verify(signatureData: ByteArray, clearData: ByteArray): Boolean = Crypto.doVerify(this.public, signatureData, clearData)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a securely random [ByteArray] of requested number of bytes. Usually used for seeds, nonces and keys.
|
||||||
|
* @param numOfBytes how many random bytes to output.
|
||||||
|
* @return a random [ByteArray].
|
||||||
|
* @throws NoSuchAlgorithmException thrown if "NativePRNGNonBlocking" is not supported on the JVM
|
||||||
|
* or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty,
|
||||||
|
* which should never happen and suggests an unusual JVM or non-standard Java library.
|
||||||
|
*/
|
||||||
|
@Throws(NoSuchAlgorithmException::class)
|
||||||
|
fun safeRandomBytes(numOfBytes: Int): ByteArray {
|
||||||
|
return safeRandom().generateSeed(numOfBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an instance of [SecureRandom] to avoid blocking, due to waiting for additional entropy, when possible.
|
||||||
|
* In this version, the NativePRNGNonBlocking is exclusively used on Linux OS to utilize dev/urandom because in high traffic
|
||||||
|
* /dev/random may wait for a certain amount of "noise" to be generated on the host machine before returning a result.
|
||||||
|
*
|
||||||
|
* On Solaris, Linux, and OS X, if the entropy gathering device in java.security is set to file:/dev/urandom
|
||||||
|
* or file:/dev/random, then NativePRNG is preferred to SHA1PRNG. Otherwise, SHA1PRNG is preferred.
|
||||||
|
* @see <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/SunProviders.html#SecureRandomImp">SecureRandom Implementation</a>.
|
||||||
|
*
|
||||||
|
* If both dev/random and dev/urandom are available, then dev/random is only preferred over dev/urandom during VM boot
|
||||||
|
* where it may be possible that OS didn't yet collect enough entropy to fill the randomness pool for the 1st time.
|
||||||
|
* @see <a href="http://www.2uo.de/myths-about-urandom/">Myths about urandom</a> for a more descriptive explanation on /dev/random Vs /dev/urandom.
|
||||||
|
* TODO: check default settings per OS and random/urandom availability.
|
||||||
|
* @return a [SecureRandom] object.
|
||||||
|
* @throws NoSuchAlgorithmException thrown if "NativePRNGNonBlocking" is not supported on the JVM
|
||||||
|
* or if no strong SecureRandom implementations are available or if Security.getProperty("securerandom.strongAlgorithms") is null or empty,
|
||||||
|
* which should never happen and suggests an unusual JVM or non-standard Java library.
|
||||||
|
*/
|
||||||
|
@Throws(NoSuchAlgorithmException::class)
|
||||||
|
fun safeRandom(): SecureRandom {
|
||||||
|
if (System.getProperty("os.name") == "Linux") {
|
||||||
|
return SecureRandom.getInstance("NativePRNGNonBlocking")
|
||||||
|
} else {
|
||||||
|
return SecureRandom.getInstanceStrong()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import java.security.KeyFactory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom [KeyFactory] for EdDSA with null security [Provider].
|
||||||
|
* This is required as a [SignatureScheme] requires a [java.security.KeyFactory] property, but i2p has
|
||||||
|
* its own KeyFactory for EdDSA, thus this actually a Proxy Pattern over i2p's KeyFactory.
|
||||||
|
*/
|
||||||
|
class EdDSAKeyFactory: KeyFactory {
|
||||||
|
constructor() : super(net.i2p.crypto.eddsa.KeyFactory(), null, "EDDSA_ED25519_SHA512")
|
||||||
|
}
|
60
core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt
Normal file
60
core/src/main/kotlin/net/corda/core/crypto/EncodingUtils.kt
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import java.nio.charset.Charset
|
||||||
|
import java.util.*
|
||||||
|
import javax.xml.bind.DatatypeConverter
|
||||||
|
|
||||||
|
|
||||||
|
// This file includes useful encoding methods and extension functions for the most common encoding/decoding operations.
|
||||||
|
|
||||||
|
// [ByteArray] encoders
|
||||||
|
|
||||||
|
fun ByteArray.toBase58(): String = Base58.encode(this)
|
||||||
|
|
||||||
|
fun ByteArray.toBase64(): String = Base64.getEncoder().encodeToString(this)
|
||||||
|
|
||||||
|
/** Convert a byte array to a hex (base 16) capitalized encoded string.*/
|
||||||
|
fun ByteArray.toHex(): String = DatatypeConverter.printHexBinary(this)
|
||||||
|
|
||||||
|
|
||||||
|
// [String] encoders and decoders
|
||||||
|
|
||||||
|
/** Base58-String to the actual real [String], i.e. "JxF12TrwUP45BMd" -> "Hello World". */
|
||||||
|
fun String.base58ToRealString() = String(base58ToByteArray(), Charset.defaultCharset())
|
||||||
|
|
||||||
|
/** Base64-String to the actual real [String], i.e. "SGVsbG8gV29ybGQ=" -> "Hello World". */
|
||||||
|
fun String.base64ToRealString() = String(base64ToByteArray())
|
||||||
|
|
||||||
|
/** HEX-String to the actual real [String], i.e. "48656C6C6F20576F726C64" -> "Hello World". */
|
||||||
|
fun String.hexToRealString() = String(hexToByteArray())
|
||||||
|
|
||||||
|
fun String.base58ToByteArray(): ByteArray = Base58.decode(this)
|
||||||
|
|
||||||
|
fun String.base64ToByteArray(): ByteArray = Base64.getDecoder().decode(this)
|
||||||
|
|
||||||
|
/** Hex-String to [ByteArray]. Accept any hex form (capitalized, lowercase, mixed). */
|
||||||
|
fun String.hexToByteArray(): ByteArray = DatatypeConverter.parseHexBinary(this);
|
||||||
|
|
||||||
|
|
||||||
|
// Encoding changers
|
||||||
|
|
||||||
|
/** Encoding changer. Base58-[String] to Base64-[String], i.e. "SGVsbG8gV29ybGQ=" -> JxF12TrwUP45BMd" */
|
||||||
|
fun String.base58toBase64(): String = base58ToByteArray().toBase64()
|
||||||
|
|
||||||
|
/** Encoding changer. Base58-[String] to Hex-[String], i.e. "SGVsbG8gV29ybGQ=" -> "48656C6C6F20576F726C64" */
|
||||||
|
fun String.base58toHex(): String = base58ToByteArray().toHex()
|
||||||
|
|
||||||
|
/** Encoding changer. Base64-[String] to Base58-[String], i.e. "SGVsbG8gV29ybGQ=" -> JxF12TrwUP45BMd" */
|
||||||
|
fun String.base64toBase58(): String = base64ToByteArray().toBase58()
|
||||||
|
|
||||||
|
/** Encoding changer. Base64-[String] to Hex-[String], i.e. "SGVsbG8gV29ybGQ=" -> "48656C6C6F20576F726C64" */
|
||||||
|
fun String.base64toHex(): String = base64ToByteArray().toHex()
|
||||||
|
|
||||||
|
/** Encoding changer. Hex-[String] to Base58-[String], i.e. "48656C6C6F20576F726C64" -> "JxF12TrwUP45BMd" */
|
||||||
|
fun String.hexToBase58(): String = hexToByteArray().toBase58()
|
||||||
|
|
||||||
|
/** Encoding changer. Hex-[String] to Base64-[String], i.e. "48656C6C6F20576F726C64" -> "SGVsbG8gV29ybGQ=" */
|
||||||
|
fun String.hexToBase64(): String = hexToByteArray().toBase64()
|
||||||
|
|
||||||
|
// Helper vars.
|
||||||
|
private val HEX_ALPHABET = "0123456789ABCDEF".toCharArray()
|
71
core/src/main/kotlin/net/corda/core/crypto/MetaData.kt
Normal file
71
core/src/main/kotlin/net/corda/core/crypto/MetaData.kt
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import net.corda.core.serialization.opaque
|
||||||
|
import net.corda.core.serialization.serialize
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.time.Instant
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [MetaData] object adds extra information to a transaction. MetaData is used to support a universal
|
||||||
|
* digital signature model enabling full, partial, fully or partially blind and metaData attached signatures,
|
||||||
|
* (such as an attached timestamp). A MetaData object contains both the merkle root of the transaction and the signer's public key.
|
||||||
|
* When signatureType is set to FULL, then visibleInputs and signedInputs can be ignored.
|
||||||
|
* Note: We could omit signatureType as it can always be defined by combining visibleInputs and signedInputs,
|
||||||
|
* but it helps to speed up the process when FULL is used, and thus we can bypass the extra check on boolean arrays.
|
||||||
|
*
|
||||||
|
* @param schemeCodeName a signature scheme's code name (e.g. ECDSA_SECP256K1_SHA256).
|
||||||
|
* @param versionID DLT's version.
|
||||||
|
* @param signatureType type of the signature, see [SignatureType] (e.g. FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND).
|
||||||
|
* @param timestamp the signature's timestamp as provided by the signer.
|
||||||
|
* @param visibleInputs for partially/fully blind signatures. We use Merkle tree boolean index flags (from left to right)
|
||||||
|
* indicating what parts of the transaction were visible when the signature was calculated.
|
||||||
|
* @param signedInputs for partial signatures. We use Merkle tree boolean index flags (from left to right)
|
||||||
|
* indicating what parts of the Merkle tree are actually signed.
|
||||||
|
* @param merkleRoot the Merkle root of the transaction.
|
||||||
|
* @param publicKey the signer's public key.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
open class MetaData(
|
||||||
|
val schemeCodeName: String,
|
||||||
|
val versionID: String,
|
||||||
|
val signatureType: SignatureType = SignatureType.FULL,
|
||||||
|
val timestamp: Instant?,
|
||||||
|
val visibleInputs: BitSet?,
|
||||||
|
val signedInputs: BitSet?,
|
||||||
|
val merkleRoot: ByteArray,
|
||||||
|
val publicKey: PublicKey) {
|
||||||
|
|
||||||
|
fun bytes() = this.serialize().bytes
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other?.javaClass != javaClass) return false
|
||||||
|
|
||||||
|
other as MetaData
|
||||||
|
|
||||||
|
if (schemeCodeName != other.schemeCodeName) return false
|
||||||
|
if (versionID != other.versionID) return false
|
||||||
|
if (signatureType != other.signatureType) return false
|
||||||
|
if (timestamp != other.timestamp) return false
|
||||||
|
if (visibleInputs != other.visibleInputs) return false
|
||||||
|
if (signedInputs != other.signedInputs) return false
|
||||||
|
if (merkleRoot.opaque() != other.merkleRoot.opaque()) return false
|
||||||
|
if (publicKey != other.publicKey) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
var result = schemeCodeName.hashCode()
|
||||||
|
result = 31 * result + versionID.hashCode()
|
||||||
|
result = 31 * result + signatureType.hashCode()
|
||||||
|
result = 31 * result + (timestamp?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (visibleInputs?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + (signedInputs?.hashCode() ?: 0)
|
||||||
|
result = 31 * result + Arrays.hashCode(merkleRoot)
|
||||||
|
result = 31 * result + publicKey.hashCode()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import net.corda.core.crypto.MerkleTree
|
|
||||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class MerkleTreeException(val reason: String) : Exception() {
|
class MerkleTreeException(val reason: String) : Exception() {
|
||||||
override fun toString() = "Partial Merkle Tree exception. Reason: $reason"
|
override fun toString() = "Partial Merkle Tree exception. Reason: $reason"
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ class MerkleTreeException(val reason: String) : Exception() {
|
|||||||
* (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree).
|
* (there can be a difference in obtained leaves ordering - that's why it's a set comparison not hashing leaves into a tree).
|
||||||
* If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15.
|
* If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class PartialMerkleTree(val root: PartialTree) {
|
class PartialMerkleTree(val root: PartialTree) {
|
||||||
/**
|
/**
|
||||||
* The structure is a little different than that of Merkle Tree.
|
* The structure is a little different than that of Merkle Tree.
|
||||||
@ -52,6 +52,7 @@ class PartialMerkleTree(val root: PartialTree) {
|
|||||||
* transaction and leaves that just keep hashes needed for calculation. Reason for this approach: during verification
|
* transaction and leaves that just keep hashes needed for calculation. Reason for this approach: during verification
|
||||||
* it's easier to extract hashes used as a base for this tree.
|
* it's easier to extract hashes used as a base for this tree.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
sealed class PartialTree {
|
sealed class PartialTree {
|
||||||
class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
||||||
class Leaf(val hash: SecureHash) : PartialTree()
|
class Leaf(val hash: SecureHash) : PartialTree()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.contracts.PartyAndReference
|
||||||
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -9,7 +11,7 @@ import java.security.PublicKey
|
|||||||
* cryptographic public key primitives into a tree structure.
|
* 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.
|
* 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 [name] "Alice" and an [owingKey] "pub1 or pub2".
|
* Her advertised [Party] then has a legal [name] "Alice" 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,
|
* [Party] is also used for service identities. E.g. Alice may also be running an interest rate oracle on her Corda node,
|
||||||
* which requires a separate signing key (and an identifying name). Services can also be distributed – run by a coordinated
|
* which requires a separate signing key (and an identifying name). Services can also be distributed – run by a coordinated
|
||||||
@ -20,8 +22,12 @@ import java.security.PublicKey
|
|||||||
*
|
*
|
||||||
* @see CompositeKey
|
* @see CompositeKey
|
||||||
*/
|
*/
|
||||||
class Party(val name: String, owningKey: CompositeKey) : AnonymousParty(owningKey) {
|
class Party(val name: String, owningKey: CompositeKey) : AbstractParty(owningKey) {
|
||||||
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
/** A helper constructor that converts the given [PublicKey] in to a [CompositeKey] with a single node */
|
||||||
constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite)
|
constructor(name: String, owningKey: PublicKey) : this(name, owningKey.composite)
|
||||||
override fun toString() = "${owningKey.toBase58String()} (name)"
|
override fun toAnonymous(): AnonymousParty = AnonymousParty(owningKey)
|
||||||
|
override fun toString() = "${owningKey.toBase58String()} (${name})"
|
||||||
|
override fun nameOrNull(): String? = name
|
||||||
|
|
||||||
|
override fun ref(bytes: OpaqueBytes): PartyAndReference = PartyAndReference(this.toAnonymous(), bytes)
|
||||||
}
|
}
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
import com.google.common.io.BaseEncoding
|
import com.google.common.io.BaseEncoding
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
@ -8,6 +9,7 @@ import java.security.MessageDigest
|
|||||||
* Container for a cryptographically secure hash value.
|
* Container for a cryptographically secure hash value.
|
||||||
* Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported).
|
* Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported).
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
sealed class SecureHash(bytes: ByteArray) : OpaqueBytes(bytes) {
|
||||||
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes) */
|
/** SHA-256 is part of the SHA-2 hash function family. Generated hash is fixed size, 256-bits (32-bytes) */
|
||||||
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
class SHA256(bytes: ByteArray) : SecureHash(bytes) {
|
||||||
|
@ -0,0 +1,42 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import java.security.*
|
||||||
|
import java.security.spec.AlgorithmParameterSpec
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is used to define a digital signature scheme.
|
||||||
|
* @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes.
|
||||||
|
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
|
||||||
|
* @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256).
|
||||||
|
* @param sig the [Signature] class that provides the functionality of a digital signature scheme.
|
||||||
|
* eg. Signature.getInstance("SHA256withECDSA", "BC").
|
||||||
|
* @param keyFactory the KeyFactory for this scheme (e.g. KeyFactory.getInstance("RSA", "BC")).
|
||||||
|
* @param keyPairGenerator defines the <i>Service Provider Interface</i> (<b>SPI</b>) for the {@code KeyPairGenerator} class.
|
||||||
|
* e.g. KeyPairGenerator.getInstance("ECDSA", "BC").
|
||||||
|
* @param algSpec parameter specs for the underlying algorithm. Note that RSA is defined by the key size rather than algSpec.
|
||||||
|
* eg. ECGenParameterSpec("secp256k1").
|
||||||
|
* @param keySize the private key size (currently used for RSA only).
|
||||||
|
* @param desc a human-readable description for this scheme.
|
||||||
|
*/
|
||||||
|
data class SignatureScheme(
|
||||||
|
val schemeNumberID: Int,
|
||||||
|
val schemeCodeName: String,
|
||||||
|
val algorithmName: String,
|
||||||
|
val sig: Signature,
|
||||||
|
val keyFactory: KeyFactory,
|
||||||
|
val keyPairGenerator: KeyPairGeneratorSpi,
|
||||||
|
val algSpec: AlgorithmParameterSpec?,
|
||||||
|
val keySize: Int,
|
||||||
|
val desc: String) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* KeyPair generators are always initialized once we create them, as no re-initialization is required.
|
||||||
|
* Note that RSA is the sole algorithm initialized specifically by its supported keySize.
|
||||||
|
*/
|
||||||
|
init {
|
||||||
|
if (algSpec != null)
|
||||||
|
keyPairGenerator.initialize(algSpec, safeRandom())
|
||||||
|
else
|
||||||
|
keyPairGenerator.initialize(keySize, safeRandom())
|
||||||
|
}
|
||||||
|
}
|
17
core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt
Normal file
17
core/src/main/kotlin/net/corda/core/crypto/SignatureType.kt
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Supported Signature types:
|
||||||
|
* <p><ul>
|
||||||
|
* <li>FULL = signature covers whole transaction, by the convention that signing the Merkle root, it is equivalent to signing all parts of the transaction.
|
||||||
|
* <li>PARTIAL = signature covers only a part of the transaction, see [MetaData].
|
||||||
|
* <li>BLIND = when an entity blindly signs without having full knowledge on the content, see [MetaData].
|
||||||
|
* <li>PARTIAL_AND_BLIND = combined PARTIAL and BLIND in the same time.
|
||||||
|
* </ul>
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
enum class SignatureType {
|
||||||
|
FULL, PARTIAL, BLIND, PARTIAL_AND_BLIND
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.core.crypto
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
@ -11,6 +12,7 @@ import java.security.SignatureException
|
|||||||
* @param raw the raw serialized data.
|
* @param raw the raw serialized data.
|
||||||
* @param sig the (unverified) signature for the data.
|
* @param sig the (unverified) signature for the data.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignature.WithKey) {
|
open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignature.WithKey) {
|
||||||
/**
|
/**
|
||||||
* Return the deserialized data if the signature can be verified.
|
* Return the deserialized data if the signature can be verified.
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import net.corda.core.serialization.opaque
|
||||||
|
import java.security.InvalidKeyException
|
||||||
|
import java.security.PublicKey
|
||||||
|
import java.security.SignatureException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper around a digital signature accompanied with metadata, see [MetaData.Full] and [DigitalSignature].
|
||||||
|
* The signature protocol works as follows: s = sign(MetaData.hashBytes).
|
||||||
|
*/
|
||||||
|
open class TransactionSignature(val signatureData: ByteArray, val metaData: MetaData) : DigitalSignature(signatureData) {
|
||||||
|
/**
|
||||||
|
* Function to auto-verify a [MetaData] object's signature.
|
||||||
|
* Note that [MetaData] contains both public key and merkle root of the transaction.
|
||||||
|
* @throws InvalidKeyException if the key is invalid.
|
||||||
|
* @throws SignatureException if this signatureData object is not initialized properly,
|
||||||
|
* the passed-in signatureData is improperly encoded or of the wrong type,
|
||||||
|
* if this signatureData algorithm is unable to process the input data provided, etc.
|
||||||
|
* @throws IllegalArgumentException if the signature scheme is not supported for this private key or if any of the clear or signature data is empty.
|
||||||
|
*/
|
||||||
|
@Throws(InvalidKeyException::class, SignatureException::class, IllegalArgumentException::class)
|
||||||
|
fun verify(): Boolean = Crypto.doVerify(metaData.publicKey, signatureData, metaData.bytes())
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exception which can be thrown by a [FlowLogic] at any point in its logic to unexpectedly bring it to a permanent end.
|
* 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
|
* The exception will propagate to all counterparty flows and will be thrown on their end the next time they wait on a
|
||||||
@ -9,6 +11,7 @@ package net.corda.core.flows
|
|||||||
* [FlowException] (or a subclass) can be a valid expected response from a flow, particularly ones which act as a service.
|
* [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.
|
* 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(override val message: String?, override val cause: Throwable?) : Exception() {
|
||||||
constructor(message: String?) : this(message, null)
|
constructor(message: String?) : this(message, null)
|
||||||
constructor(cause: Throwable?) : this(cause?.toString(), cause)
|
constructor(cause: Throwable?) : this(cause?.toString(), cause)
|
||||||
|
@ -7,6 +7,7 @@ import net.corda.core.node.ServiceHub
|
|||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
|
import net.corda.core.utilities.debug
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
@ -135,7 +136,9 @@ abstract class FlowLogic<out T> {
|
|||||||
if (shareParentSessions) {
|
if (shareParentSessions) {
|
||||||
subLogic.sessionFlow = this
|
subLogic.sessionFlow = this
|
||||||
}
|
}
|
||||||
|
logger.debug { "Calling subflow: $subLogic" }
|
||||||
val result = subLogic.call()
|
val result = subLogic.call()
|
||||||
|
logger.debug { "Subflow finished with result $result" }
|
||||||
// It's easy to forget this when writing flows so we just step it to the DONE state when it completes.
|
// It's easy to forget this when writing flows so we just step it to the DONE state when it completes.
|
||||||
subLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
subLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||||
return result
|
return result
|
||||||
|
@ -2,6 +2,7 @@ package net.corda.core.flows
|
|||||||
|
|
||||||
import com.google.common.primitives.Primitives
|
import com.google.common.primitives.Primitives
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||||
import java.lang.reflect.ParameterizedType
|
import java.lang.reflect.ParameterizedType
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
@ -23,8 +24,7 @@ import kotlin.reflect.primaryConstructor
|
|||||||
* TODO: Align with API related logic for passing in FlowLogic references (FlowRef)
|
* TODO: Align with API related logic for passing in FlowLogic references (FlowRef)
|
||||||
* TODO: Actual support for AppContext / AttachmentsClassLoader
|
* TODO: Actual support for AppContext / AttachmentsClassLoader
|
||||||
*/
|
*/
|
||||||
class FlowLogicRefFactory(private val flowWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken() {
|
class FlowLogicRefFactory(val flowWhitelist: Map<String, Set<String>>) : SingletonSerializeAsToken() {
|
||||||
|
|
||||||
constructor() : this(mapOf())
|
constructor() : this(mapOf())
|
||||||
|
|
||||||
// Pending real dependence on AppContext for class loading etc
|
// Pending real dependence on AppContext for class loading etc
|
||||||
@ -186,6 +186,7 @@ class FlowLogicRefFactory(private val flowWhitelist: Map<String, Set<String>>) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentException("${FlowLogicRef::class.java.simpleName} cannot be constructed for ${FlowLogic::class.java.simpleName} of type ${type.name} $msg")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -194,12 +195,14 @@ class IllegalFlowLogicException(type: Class<*>, msg: String) : IllegalArgumentEx
|
|||||||
* Only allows a String reference to the FlowLogic class, and only allows restricted argument types as per [FlowLogicRefFactory].
|
* Only allows a String reference to the FlowLogic class, and only allows restricted argument types as per [FlowLogicRefFactory].
|
||||||
*/
|
*/
|
||||||
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
// TODO: align this with the existing [FlowRef] in the bank-side API (probably replace some of the API classes)
|
||||||
|
@CordaSerializable
|
||||||
data class FlowLogicRef internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map<String, Any?>)
|
data class FlowLogicRef internal constructor(val flowLogicClassName: String, val appContext: AppContext, val args: Map<String, Any?>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
* This is just some way to track what attachments need to be in the class loader, but may later include some app
|
||||||
* properties loaded from the attachments. And perhaps the authenticated user for an API call?
|
* properties loaded from the attachments. And perhaps the authenticated user for an API call?
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class AppContext(val attachments: List<SecureHash>) {
|
data class AppContext(val attachments: List<SecureHash>) {
|
||||||
// TODO: build a real [AttachmentsClassLoader] etc
|
// TODO: build a real [AttachmentsClassLoader] etc
|
||||||
val classLoader: ClassLoader
|
val classLoader: ClassLoader
|
||||||
|
@ -5,6 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import org.slf4j.Logger
|
import org.slf4j.Logger
|
||||||
@ -14,6 +15,7 @@ import java.util.*
|
|||||||
* A unique identifier for a single state machine run, valid across node restarts. Note that a single run always
|
* 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.
|
* has at least one flow, but that flow may also invoke sub-flows: they all share the same run id.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class StateMachineRunId private constructor(val uuid: UUID) {
|
data class StateMachineRunId private constructor(val uuid: UUID) {
|
||||||
companion object {
|
companion object {
|
||||||
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
|
fun createRandom(): StateMachineRunId = StateMachineRunId(UUID.randomUUID())
|
||||||
|
@ -14,22 +14,26 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.node.services.NetworkMapCache
|
import net.corda.core.node.services.NetworkMapCache
|
||||||
import net.corda.core.node.services.StateMachineTransactionMapping
|
import net.corda.core.node.services.StateMachineTransactionMapping
|
||||||
import net.corda.core.node.services.Vault
|
import net.corda.core.node.services.Vault
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
data class StateMachineInfo(
|
data class StateMachineInfo(
|
||||||
val id: StateMachineRunId,
|
val id: StateMachineRunId,
|
||||||
val flowLogicClassName: String,
|
val flowLogicClassName: String,
|
||||||
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?
|
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
sealed class StateMachineUpdate(val id: StateMachineRunId) {
|
sealed class StateMachineUpdate(val id: StateMachineRunId) {
|
||||||
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) {
|
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) {
|
||||||
override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})"
|
override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})"
|
||||||
}
|
}
|
||||||
|
|
||||||
class Removed(id: StateMachineRunId) : StateMachineUpdate(id) {
|
class Removed(id: StateMachineRunId) : StateMachineUpdate(id) {
|
||||||
override fun toString() = "Removed($id)"
|
override fun toString() = "Removed($id)"
|
||||||
}
|
}
|
||||||
@ -107,12 +111,17 @@ interface CordaRPCOps : RPCOps {
|
|||||||
*/
|
*/
|
||||||
fun attachmentExists(id: SecureHash): Boolean
|
fun attachmentExists(id: SecureHash): Boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Download an attachment JAR by ID
|
||||||
|
*/
|
||||||
|
fun openAttachment(id: SecureHash): InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Uploads a jar to the node, returns it's hash.
|
* Uploads a jar to the node, returns it's hash.
|
||||||
*/
|
*/
|
||||||
fun uploadAttachment(jar: InputStream): SecureHash
|
fun uploadAttachment(jar: InputStream): SecureHash
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
// TODO: Remove this from the interface
|
||||||
@Deprecated("This service will be removed in a future milestone")
|
@Deprecated("This service will be removed in a future milestone")
|
||||||
fun uploadFile(dataType: String, name: String?, file: InputStream): String
|
fun uploadFile(dataType: String, name: String?, file: InputStream): String
|
||||||
|
|
||||||
@ -122,7 +131,7 @@ interface CordaRPCOps : RPCOps {
|
|||||||
* Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass].
|
* Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass].
|
||||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator].
|
* This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator].
|
||||||
*/
|
*/
|
||||||
fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>)
|
fun authoriseContractUpgrade(state: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorise a contract state upgrade.
|
* Authorise a contract state upgrade.
|
||||||
@ -153,6 +162,9 @@ interface CordaRPCOps : RPCOps {
|
|||||||
* Returns the [Party] with the given name as it's [Party.name]
|
* Returns the [Party] with the given name as it's [Party.name]
|
||||||
*/
|
*/
|
||||||
fun partyFromName(name: String): Party?
|
fun partyFromName(name: String): Party?
|
||||||
|
|
||||||
|
/** Enumerates the class names of the flows that this node knows about. */
|
||||||
|
fun registeredFlows(): List<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -206,6 +218,7 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlow
|
|||||||
* @param progress The stream of progress tracker events.
|
* @param progress The stream of progress tracker events.
|
||||||
* @param returnValue A [ListenableFuture] of the flow's return value.
|
* @param returnValue A [ListenableFuture] of the flow's return value.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class FlowHandle<A>(
|
data class FlowHandle<A>(
|
||||||
val id: StateMachineRunId,
|
val id: StateMachineRunId,
|
||||||
val progress: Observable<String>,
|
val progress: Observable<String>,
|
||||||
|
@ -5,6 +5,7 @@ import com.google.common.util.concurrent.SettableFuture
|
|||||||
import net.corda.core.catch
|
import net.corda.core.catch
|
||||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||||
import net.corda.core.node.services.PartyInfo
|
import net.corda.core.node.services.PartyInfo
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
|
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
@ -160,6 +161,7 @@ interface MessageHandlerRegistration
|
|||||||
* @param sessionID identifier for the session the message is part of. For services listening before
|
* @param sessionID identifier for the session the message is part of. For services listening before
|
||||||
* a session is established, use [DEFAULT_SESSION_ID].
|
* a session is established, use [DEFAULT_SESSION_ID].
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) {
|
data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) {
|
||||||
fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID
|
fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID
|
||||||
override fun toString(): String = "$topic.$sessionID"
|
override fun toString(): String = "$topic.$sessionID"
|
||||||
@ -213,4 +215,5 @@ interface AllPossibleRecipients : MessageRecipients
|
|||||||
* A general Ack message that conveys no content other than it's presence for use when you want an acknowledgement
|
* A general Ack message that conveys no content other than it's presence for use when you want an acknowledgement
|
||||||
* from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response.
|
* from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
object Ack : DeserializeAsKotlinObjectDef
|
object Ack : DeserializeAsKotlinObjectDef
|
||||||
|
@ -2,6 +2,7 @@ package net.corda.core.node
|
|||||||
|
|
||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
@ -24,6 +25,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
|||||||
private val pathsToAttachments = HashMap<String, Attachment>()
|
private val pathsToAttachments = HashMap<String, Attachment>()
|
||||||
private val idsToAttachments = HashMap<SecureHash, Attachment>()
|
private val idsToAttachments = HashMap<SecureHash, Attachment>()
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class OverlappingAttachments(val path: String) : Exception() {
|
class OverlappingAttachments(val path: String) : Exception() {
|
||||||
override fun toString() = "Multiple attachments define a file at path $path"
|
override fun toString() = "Multiple attachments define a file at path $path"
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package net.corda.core.node
|
package net.corda.core.node
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
|
||||||
import net.corda.core.messaging.CordaRPCOps
|
import net.corda.core.messaging.CordaRPCOps
|
||||||
|
import net.corda.core.serialization.SerializationCustomization
|
||||||
import java.util.function.Function
|
import java.util.function.Function
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -40,14 +40,12 @@ abstract class CordaPluginRegistry(
|
|||||||
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList()
|
open val servicePlugins: List<Function<PluginServiceHub, out Any>> = emptyList()
|
||||||
) {
|
) {
|
||||||
/**
|
/**
|
||||||
* Optionally register types with [Kryo] for use over RPC, as we lock down the types that can be serialised in this
|
* Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized.
|
||||||
* particular use case.
|
|
||||||
* For example, if you add an RPC interface that carries some contract states back and forth, you need to register
|
|
||||||
* those classes here using the [register] method on Kryo.
|
|
||||||
*
|
|
||||||
* TODO: Kryo and likely the requirement to register classes here will go away when we replace the serialization implementation.
|
|
||||||
*
|
*
|
||||||
|
* For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by
|
||||||
|
* adding the @CordaSerializable annotation or via this method.
|
||||||
|
**
|
||||||
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
* @return true if you register types, otherwise you will be filtered out of the list of plugins considered in future.
|
||||||
*/
|
*/
|
||||||
open fun registerRPCKryoTypes(kryo: Kryo): Boolean = false
|
open fun customizeSerialization(custom: SerializationCustomization): Boolean = false
|
||||||
}
|
}
|
@ -4,18 +4,22 @@ import net.corda.core.crypto.Party
|
|||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
import net.corda.core.node.services.ServiceInfo
|
import net.corda.core.node.services.ServiceInfo
|
||||||
import net.corda.core.node.services.ServiceType
|
import net.corda.core.node.services.ServiceType
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Information for an advertised service including the service specific identity information.
|
* 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
|
* 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: Party)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Info about a network node that acts on behalf of some form of contract party.
|
* Info about a network node that acts on behalf of some form of contract party.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class NodeInfo(val address: SingleMessageRecipient,
|
data class NodeInfo(val address: SingleMessageRecipient,
|
||||||
val legalIdentity: Party,
|
val legalIdentity: Party,
|
||||||
|
val version: Version,
|
||||||
var advertisedServices: List<ServiceEntry> = emptyList(),
|
var advertisedServices: List<ServiceEntry> = emptyList(),
|
||||||
val physicalLocation: PhysicalLocation? = null) {
|
val physicalLocation: PhysicalLocation? = null) {
|
||||||
init {
|
init {
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
package net.corda.core.node
|
package net.corda.core.node
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
/** A latitude/longitude pair. */
|
/** A latitude/longitude pair. */
|
||||||
|
@CordaSerializable
|
||||||
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
||||||
init {
|
init {
|
||||||
require(latitude in -90..90)
|
require(latitude in -90..90)
|
||||||
@ -39,6 +41,7 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
|||||||
* A labelled [WorldCoordinate], where the label is human meaningful. For example, the name of the nearest city.
|
* A labelled [WorldCoordinate], where the label is human meaningful. For example, the name of the nearest city.
|
||||||
* Labels should not refer to non-landmarks, for example, they should not contain the names of organisations.
|
* Labels should not refer to non-landmarks, for example, they should not contain the names of organisations.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String)
|
data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -9,6 +9,25 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subset of node services that are used for loading transactions from the wire into fully resolved, looked up
|
||||||
|
* forms ready for verification.
|
||||||
|
*
|
||||||
|
* @see ServiceHub
|
||||||
|
*/
|
||||||
|
interface ServicesForResolution {
|
||||||
|
val identityService: IdentityService
|
||||||
|
val storageService: AttachmentsStorageService
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
||||||
|
*
|
||||||
|
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
|
||||||
|
*/
|
||||||
|
@Throws(TransactionResolutionException::class)
|
||||||
|
fun loadState(stateRef: StateRef): TransactionState<*>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service hub simply vends references to the other services a node has. Some of those services may be missing or
|
* A service hub simply vends references to the other services a node has. Some of those services may be missing or
|
||||||
* mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of
|
* mocked out. This class is useful to pass to chunks of pluggable code that might have need of many different kinds of
|
||||||
@ -17,12 +36,11 @@ import java.time.Clock
|
|||||||
* Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid their internal
|
* Any services exposed to flows (public view) need to implement [SerializeAsToken] or similar to avoid their internal
|
||||||
* state from being serialized in checkpoints.
|
* state from being serialized in checkpoints.
|
||||||
*/
|
*/
|
||||||
interface ServiceHub {
|
interface ServiceHub : ServicesForResolution {
|
||||||
val vaultService: VaultService
|
val vaultService: VaultService
|
||||||
val keyManagementService: KeyManagementService
|
val keyManagementService: KeyManagementService
|
||||||
val identityService: IdentityService
|
|
||||||
val storageService: StorageService
|
|
||||||
val networkService: MessagingService
|
val networkService: MessagingService
|
||||||
|
override val storageService: StorageService
|
||||||
val networkMapCache: NetworkMapCache
|
val networkMapCache: NetworkMapCache
|
||||||
val schedulerService: SchedulerService
|
val schedulerService: SchedulerService
|
||||||
val clock: Clock
|
val clock: Clock
|
||||||
@ -37,20 +55,29 @@ interface ServiceHub {
|
|||||||
// TODO: Make this take a single tx.
|
// TODO: Make this take a single tx.
|
||||||
fun recordTransactions(txs: Iterable<SignedTransaction>)
|
fun recordTransactions(txs: Iterable<SignedTransaction>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given some [SignedTransaction]s, writes them to the local storage for validated transactions and then
|
||||||
|
* sends them to the vault for further processing.
|
||||||
|
*
|
||||||
|
* @param txs The transactions to record.
|
||||||
|
*/
|
||||||
|
fun recordTransactions(vararg txs: SignedTransaction) = recordTransactions(txs.toList())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
|
||||||
*
|
*
|
||||||
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
|
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
|
||||||
*/
|
*/
|
||||||
fun loadState(stateRef: StateRef): TransactionState<*> {
|
@Throws(TransactionResolutionException::class)
|
||||||
|
override fun loadState(stateRef: StateRef): TransactionState<*> {
|
||||||
val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
|
||||||
return definingTx.tx.outputs[stateRef.index]
|
return definingTx.tx.outputs[stateRef.index]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Given a [StateRef] loads the referenced transaction and returns a [StateAndRef<T>]
|
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the protocol.
|
||||||
*
|
*
|
||||||
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
|
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args].
|
||||||
*/
|
*/
|
||||||
fun <T : ContractState> toStateAndRef(ref: StateRef): StateAndRef<T> {
|
fun <T : ContractState> toStateAndRef(ref: StateRef): StateAndRef<T> {
|
||||||
val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash)
|
val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash)
|
||||||
|
28
core/src/main/kotlin/net/corda/core/node/Version.kt
Normal file
28
core/src/main/kotlin/net/corda/core/node/Version.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.core.node
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versions of the same [major] version but with different [minor] versions are considered compatible with each other. One
|
||||||
|
* exception to this is when the major version is 0 - each different minor version should be considered incompatible.
|
||||||
|
*
|
||||||
|
* If two [Version]s are equal (i.e. [equals] returns true) but they are both [snapshot] then they may refer to different
|
||||||
|
* builds of the node. [NodeVersionInfo.revision] would be required to differentiate the two.
|
||||||
|
*/
|
||||||
|
@CordaSerializable
|
||||||
|
data class Version(val major: Int, val minor: Int, val snapshot: Boolean) {
|
||||||
|
companion object {
|
||||||
|
private val pattern = Pattern.compile("""(\d+)\.(\d+)(-SNAPSHOT)?""")
|
||||||
|
|
||||||
|
fun parse(string: String): Version {
|
||||||
|
val matcher = pattern.matcher(string)
|
||||||
|
require(matcher.matches())
|
||||||
|
return Version(matcher.group(1).toInt(), matcher.group(2).toInt(), matcher.group(3) != null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = if (snapshot) "$major.$minor-SNAPSHOT" else "$major.$minor"
|
||||||
|
}
|
||||||
|
|
||||||
|
data class NodeVersionInfo(val version: Version, val revision: String, val vendor: String)
|
@ -3,11 +3,20 @@ package net.corda.core.node.services
|
|||||||
import net.corda.core.contracts.Attachment
|
import net.corda.core.contracts.Attachment
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An attachment store records potentially large binary objects, identified by their hash.
|
* An attachment store records potentially large binary objects, identified by their hash.
|
||||||
*/
|
*/
|
||||||
interface AttachmentStorage {
|
interface AttachmentStorage {
|
||||||
|
/**
|
||||||
|
* If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for
|
||||||
|
* human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory
|
||||||
|
* will not have any effect).
|
||||||
|
*/
|
||||||
|
var automaticallyExtractAttachments : Boolean
|
||||||
|
var storePath : Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
|
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
|
||||||
* a stream for the data, which will be a zip/jar file.
|
* a stream for the data, which will be a zip/jar file.
|
||||||
|
@ -5,12 +5,11 @@ import com.google.common.util.concurrent.ListenableFuture
|
|||||||
import net.corda.core.contracts.Contract
|
import net.corda.core.contracts.Contract
|
||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.messaging.MessageRecipients
|
|
||||||
import net.corda.core.messaging.MessagingService
|
import net.corda.core.messaging.MessagingService
|
||||||
import net.corda.core.messaging.SingleMessageRecipient
|
import net.corda.core.messaging.SingleMessageRecipient
|
||||||
import net.corda.core.node.NodeInfo
|
import net.corda.core.node.NodeInfo
|
||||||
import net.corda.core.node.ServiceEntry
|
|
||||||
import net.corda.core.randomOrNull
|
import net.corda.core.randomOrNull
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,6 +20,7 @@ import rx.Observable
|
|||||||
*/
|
*/
|
||||||
interface NetworkMapCache {
|
interface NetworkMapCache {
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
sealed class MapChange(val node: NodeInfo) {
|
sealed class MapChange(val node: NodeInfo) {
|
||||||
class Added(node: NodeInfo) : MapChange(node)
|
class Added(node: NodeInfo) : MapChange(node)
|
||||||
class Removed(node: NodeInfo) : MapChange(node)
|
class Removed(node: NodeInfo) : MapChange(node)
|
||||||
@ -73,6 +73,7 @@ interface NetworkMapCache {
|
|||||||
|
|
||||||
/** Look up the node info for a specific peer key. */
|
/** Look up the node info for a specific peer key. */
|
||||||
fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo?
|
fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo?
|
||||||
|
|
||||||
/** Look up all nodes advertising the service owned by [compositeKey] */
|
/** Look up all nodes advertising the service owned by [compositeKey] */
|
||||||
fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> {
|
fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> {
|
||||||
return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } }
|
return partyNodes.filter { it.advertisedServices.any { it.identity.owningKey == compositeKey } }
|
||||||
@ -108,6 +109,11 @@ interface NetworkMapCache {
|
|||||||
/** Checks whether a given party is an advertised notary identity */
|
/** Checks whether a given party is an advertised notary identity */
|
||||||
fun isNotary(party: Party): Boolean = notaryNodes.any { it.notaryIdentity == party }
|
fun isNotary(party: Party): Boolean = notaryNodes.any { it.notaryIdentity == party }
|
||||||
|
|
||||||
|
/** Checks whether a given party is an advertised validating notary identity */
|
||||||
|
fun isValidatingNotary(party: Party): Boolean {
|
||||||
|
return notaryNodes.any { it.notaryIdentity == party && it.advertisedServices.any { it.info.type.isValidatingNotary() }}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a network map service; fetches a copy of the latest map from the service and subscribes to any further
|
* Add a network map service; fetches a copy of the latest map from the service and subscribes to any further
|
||||||
* updates.
|
* updates.
|
||||||
@ -138,6 +144,7 @@ interface NetworkMapCache {
|
|||||||
fun runWithoutMapService()
|
fun runWithoutMapService()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
sealed class NetworkCacheError : Exception() {
|
sealed class NetworkCacheError : Exception() {
|
||||||
/** Indicates a failure to deregister, because of a rejected request from the remote node */
|
/** Indicates a failure to deregister, because of a rejected request from the remote node */
|
||||||
class DeregistrationFailed : NetworkCacheError()
|
class DeregistrationFailed : NetworkCacheError()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A container for additional information for an advertised service.
|
* A container for additional information for an advertised service.
|
||||||
*
|
*
|
||||||
@ -7,6 +9,7 @@ package net.corda.core.node.services
|
|||||||
* @param name the service name, used for differentiating multiple services of the same type. Can also be used as a
|
* @param name the service name, used for differentiating multiple services of the same type. Can also be used as a
|
||||||
* grouping identifier for nodes collectively running a distributed service.
|
* grouping identifier for nodes collectively running a distributed service.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class ServiceInfo(val type: ServiceType, val name: String? = null) {
|
data class ServiceInfo(val type: ServiceType, val name: String? = null) {
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(encoded: String): ServiceInfo {
|
fun parse(encoded: String): ServiceInfo {
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
package net.corda.core.node.services
|
package net.corda.core.node.services
|
||||||
|
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Identifier for service types a node can expose over the network to other peers. These types are placed into network
|
* Identifier for service types a node can expose over the network to other peers. These types are placed into network
|
||||||
* map advertisements. Services that are purely local and are not providing functionality to other parts of the network
|
* map advertisements. Services that are purely local and are not providing functionality to other parts of the network
|
||||||
* don't need a declared service type.
|
* don't need a declared service type.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
sealed class ServiceType(val id: String) {
|
sealed class ServiceType(val id: String) {
|
||||||
init {
|
init {
|
||||||
// Enforce:
|
// Enforce:
|
||||||
@ -43,6 +46,7 @@ sealed class ServiceType(val id: String) {
|
|||||||
|
|
||||||
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
|
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
|
||||||
fun isNotary() = isSubTypeOf(notary)
|
fun isNotary() = isSubTypeOf(notary)
|
||||||
|
fun isValidatingNotary() = isNotary() && id.contains(".validating")
|
||||||
|
|
||||||
override fun hashCode(): Int = id.hashCode()
|
override fun hashCode(): Int = id.hashCode()
|
||||||
override fun toString(): String = id.toString()
|
override fun toString(): String = id.toString()
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
|||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.toFuture
|
import net.corda.core.toFuture
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -32,13 +33,12 @@ val DEFAULT_SESSION_ID = 0L
|
|||||||
*
|
*
|
||||||
* This abstract class has no references to Cash contracts.
|
* This abstract class has no references to Cash contracts.
|
||||||
*
|
*
|
||||||
* [states] Holds the states that are *active* and *relevant*.
|
* [states] Holds a [VaultService] queried subset of states that are *active* and *relevant*.
|
||||||
* Active means they haven't been consumed yet (or we don't know about it).
|
* Active means they haven't been consumed yet (or we don't know about it).
|
||||||
* Relevant means they contain at least one of our pubkeys.
|
* Relevant means they contain at least one of our pubkeys.
|
||||||
*/
|
*/
|
||||||
class Vault(val states: List<StateAndRef<ContractState>>) {
|
@CordaSerializable
|
||||||
@Suppress("UNCHECKED_CAST")
|
class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||||
inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>>
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents an update observed by the vault that will be notified to observers. Include the [StateRef]s of
|
* Represents an update observed by the vault that will be notified to observers. Include the [StateRef]s of
|
||||||
@ -48,6 +48,7 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
|||||||
* If the vault observes multiple transactions simultaneously, where some transactions consume the outputs of some of the
|
* If the vault observes multiple transactions simultaneously, where some transactions consume the outputs of some of the
|
||||||
* other transactions observed, then the changes are observed "net" of those.
|
* other transactions observed, then the changes are observed "net" of those.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Update(val consumed: Set<StateAndRef<ContractState>>, val produced: Set<StateAndRef<ContractState>>) {
|
data class Update(val consumed: Set<StateAndRef<ContractState>>, val produced: Set<StateAndRef<ContractState>>) {
|
||||||
/** Checks whether the update contains a state of the specified type. */
|
/** Checks whether the update contains a state of the specified type. */
|
||||||
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
|
inline fun <reified T : ContractState> containsType() = consumed.any { it.state.data is T } || produced.any { it.state.data is T }
|
||||||
@ -81,6 +82,10 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
|||||||
companion object {
|
companion object {
|
||||||
val NoUpdate = Update(emptySet(), emptySet())
|
val NoUpdate = Update(emptySet(), emptySet())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class StateStatus {
|
||||||
|
UNCONSUMED, CONSUMED
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -92,11 +97,6 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
|||||||
* Note that transactions we've seen are held by the storage service, not the vault.
|
* Note that transactions we've seen are held by the storage service, not the vault.
|
||||||
*/
|
*/
|
||||||
interface VaultService {
|
interface VaultService {
|
||||||
/**
|
|
||||||
* Returns a read-only snapshot of the vault at the time the call is made. Note that if you consume states or
|
|
||||||
* keys in this vault, you must inform the vault service so it can update its internal state.
|
|
||||||
*/
|
|
||||||
val currentVault: Vault
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prefer the use of [updates] unless you know why you want to use this instead.
|
* Prefer the use of [updates] unless you know why you want to use this instead.
|
||||||
@ -125,25 +125,13 @@ interface VaultService {
|
|||||||
* Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the
|
* Atomically get the current vault and a stream of updates. Note that the Observable buffers updates until the
|
||||||
* first subscriber is registered so as to avoid racing with early updates.
|
* first subscriber is registered so as to avoid racing with early updates.
|
||||||
*/
|
*/
|
||||||
fun track(): Pair<Vault, Observable<Vault.Update>>
|
fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a snapshot of the heads of LinearStates.
|
* Return unconsumed [ContractState]s for a given set of [StateRef]s
|
||||||
|
* TODO: revisit and generalize this exposed API function.
|
||||||
*/
|
*/
|
||||||
val linearHeads: Map<UniqueIdentifier, StateAndRef<LinearState>>
|
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?>
|
||||||
|
|
||||||
// TODO: When KT-10399 is fixed, rename this and remove the inline version below.
|
|
||||||
|
|
||||||
/** Returns the [linearHeads] only when the type of the state would be considered an 'instanceof' the given type. */
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun <T : LinearState> linearHeadsOfType_(stateType: Class<T>): Map<UniqueIdentifier, StateAndRef<T>> {
|
|
||||||
return linearHeads.filterValues { stateType.isInstance(it.state.data) }.mapValues { StateAndRef(it.value.state as TransactionState<T>, it.value.ref) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?> {
|
|
||||||
val refsToStates = currentVault.states.associateBy { it.ref }
|
|
||||||
return refs.associateBy({ it }) { refsToStates[it]?.state }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Possibly update the vault by marking as spent states that these transactions consume, and adding any relevant
|
* Possibly update the vault by marking as spent states that these transactions consume, and adding any relevant
|
||||||
@ -165,7 +153,7 @@ interface VaultService {
|
|||||||
|
|
||||||
/** Get contracts we would be willing to upgrade the suggested contract to. */
|
/** Get contracts we would be willing to upgrade the suggested contract to. */
|
||||||
// TODO: We need a better place to put business logic functions
|
// TODO: We need a better place to put business logic functions
|
||||||
fun getAuthorisedContractUpgrade(ref: StateRef): Class<UpgradedContract<*, *>>?
|
fun getAuthorisedContractUpgrade(ref: StateRef): Class<out UpgradedContract<*, *>>?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorise a contract state upgrade.
|
* Authorise a contract state upgrade.
|
||||||
@ -173,7 +161,7 @@ interface VaultService {
|
|||||||
* Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass].
|
* Invoking this method indicate the node is willing to upgrade the [state] using the [upgradedContractClass].
|
||||||
* This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator].
|
* This method will NOT initiate the upgrade process. To start the upgrade process, see [ContractUpgradeFlow.Instigator].
|
||||||
*/
|
*/
|
||||||
fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>)
|
fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authorise a contract state upgrade.
|
* Authorise a contract state upgrade.
|
||||||
@ -212,11 +200,26 @@ interface VaultService {
|
|||||||
fun generateSpend(tx: TransactionBuilder,
|
fun generateSpend(tx: TransactionBuilder,
|
||||||
amount: Amount<Currency>,
|
amount: Amount<Currency>,
|
||||||
to: CompositeKey,
|
to: CompositeKey,
|
||||||
onlyFromParties: Set<AnonymousParty>? = null): Pair<TransactionBuilder, List<CompositeKey>>
|
onlyFromParties: Set<AbstractParty>? = null): Pair<TransactionBuilder, List<CompositeKey>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [ContractState]s of a given [Contract] type and list of [Vault.StateStatus]
|
||||||
|
*/
|
||||||
|
fun <T : ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>): List<StateAndRef<T>>
|
||||||
}
|
}
|
||||||
|
|
||||||
inline fun <reified T : LinearState> VaultService.linearHeadsOfType() = linearHeadsOfType_(T::class.java)
|
inline fun <reified T: ContractState> VaultService.unconsumedStates(): List<StateAndRef<T>> =
|
||||||
inline fun <reified T : DealState> VaultService.dealsWith(party: Party) = linearHeadsOfType<T>().values.filter {
|
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.UNCONSUMED))
|
||||||
|
|
||||||
|
inline fun <reified T: ContractState> VaultService.consumedStates(): List<StateAndRef<T>> =
|
||||||
|
states(setOf(T::class.java), EnumSet.of(Vault.StateStatus.CONSUMED))
|
||||||
|
|
||||||
|
/** Returns the [linearState] heads only when the type of the state would be considered an 'instanceof' the given type. */
|
||||||
|
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 }
|
||||||
|
|
||||||
|
inline fun <reified T : DealState> VaultService.dealsWith(party: AbstractParty) = linearHeadsOfType<T>().values.filter {
|
||||||
it.state.data.parties.any { it == party }
|
it.state.data.parties.any { it == party }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,12 +264,17 @@ interface FileUploader {
|
|||||||
fun accepts(type: String): Boolean
|
fun accepts(type: String): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AttachmentsStorageService {
|
||||||
|
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
|
||||||
|
val attachments: AttachmentStorage
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like
|
* A sketch of an interface to a simple key/value storage system. Intended for persistence of simple blobs like
|
||||||
* transactions, serialised flow state machines and so on. Again, this isn't intended to imply lack of SQL or
|
* transactions, serialised flow state machines and so on. Again, this isn't intended to imply lack of SQL or
|
||||||
* anything like that, this interface is only big enough to support the prototyping work.
|
* anything like that, this interface is only big enough to support the prototyping work.
|
||||||
*/
|
*/
|
||||||
interface StorageService {
|
interface StorageService : AttachmentsStorageService {
|
||||||
/**
|
/**
|
||||||
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.
|
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.
|
||||||
* The signatures aren't technically needed after that point, but we keep them around so that we can relay
|
* The signatures aren't technically needed after that point, but we keep them around so that we can relay
|
||||||
@ -274,9 +282,6 @@ interface StorageService {
|
|||||||
*/
|
*/
|
||||||
val validatedTransactions: ReadOnlyTransactionStorage
|
val validatedTransactions: ReadOnlyTransactionStorage
|
||||||
|
|
||||||
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
|
|
||||||
val attachments: AttachmentStorage
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
@Deprecated("This service will be removed in a future milestone")
|
@Deprecated("This service will be removed in a future milestone")
|
||||||
val uploaders: List<FileUploader>
|
val uploaders: List<FileUploader>
|
||||||
|
@ -2,8 +2,10 @@ package net.corda.core.node.services
|
|||||||
|
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.StateMachineRunId
|
import net.corda.core.flows.StateMachineRunId
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash)
|
data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
|||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that records input states of the given transaction and provides conflict information
|
* A service that records input states of the given transaction and provides conflict information
|
||||||
@ -15,6 +16,7 @@ interface UniquenessProvider {
|
|||||||
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
||||||
|
|
||||||
/** Specifies the consuming transaction for every conflicting state */
|
/** Specifies the consuming transaction for every conflicting state */
|
||||||
|
@CordaSerializable
|
||||||
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
data class Conflict(val stateHistory: Map<StateRef, ConsumingTx>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -26,7 +28,9 @@ interface UniquenessProvider {
|
|||||||
* This allows a party to just submit invalid transactions with outputs it was aware of and
|
* This allows a party to just submit invalid transactions with outputs it was aware of and
|
||||||
* find out where exactly they were spent.
|
* find out where exactly they were spent.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package net.corda.core.schemas
|
package net.corda.core.schemas
|
||||||
|
|
||||||
|
import io.requery.Persistable
|
||||||
import net.corda.core.contracts.ContractState
|
import net.corda.core.contracts.ContractState
|
||||||
import net.corda.core.contracts.StateRef
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.serialization.toHexString
|
import net.corda.core.serialization.toHexString
|
||||||
@ -48,7 +49,7 @@ abstract class MappedSchema(schemaFamily: Class<*>,
|
|||||||
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||||
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||||
*/
|
*/
|
||||||
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null)
|
@MappedSuperclass open class PersistentState(@EmbeddedId var stateRef: PersistentStateRef? = null) : Persistable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Embedded [StateRef] representation used in state mapping.
|
* Embedded [StateRef] representation used in state mapping.
|
||||||
@ -62,5 +63,9 @@ data class PersistentStateRef(
|
|||||||
var index: Int?
|
var index: Int?
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index)
|
constructor(stateRef: StateRef) : this(stateRef.txhash.bytes.toHexString(), stateRef.index)
|
||||||
|
/*
|
||||||
|
JPA Query requirement:
|
||||||
|
@Entity classes should have a default (non-arg) constructor to instantiate the objects when retrieving them from the database.
|
||||||
|
*/
|
||||||
constructor() : this(null, null)
|
constructor() : this(null, null)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
package net.corda.core.schemas.requery
|
||||||
|
|
||||||
|
import io.requery.Key
|
||||||
|
import io.requery.Persistable
|
||||||
|
import io.requery.Superclass
|
||||||
|
import net.corda.core.contracts.StateRef
|
||||||
|
|
||||||
|
import javax.persistence.Column
|
||||||
|
|
||||||
|
object Requery {
|
||||||
|
/**
|
||||||
|
* A super class for all mapped states exported to a schema that ensures the [StateRef] appears on the database row. The
|
||||||
|
* [StateRef] will be set to the correct value by the framework (there's no need to set during mapping generation by the state itself).
|
||||||
|
*/
|
||||||
|
// TODO: this interface will supercede the existing [PersistentState] interface defined in PersistentTypes.kt
|
||||||
|
// once we cut-over all existing Hibernate ContractState persistence to Requery
|
||||||
|
@Superclass interface PersistentState : Persistable {
|
||||||
|
@get:Key
|
||||||
|
@get:Column(name = "transaction_id", length = 64)
|
||||||
|
var txId: String
|
||||||
|
|
||||||
|
@get:Key
|
||||||
|
@get:Column(name = "output_index")
|
||||||
|
var index: Int
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.core.schemas.requery.converters
|
||||||
|
|
||||||
|
import io.requery.Converter
|
||||||
|
import java.sql.Blob
|
||||||
|
import javax.sql.rowset.serial.SerialBlob
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts from a [ByteArray] to a [Blob].
|
||||||
|
*/
|
||||||
|
class BlobConverter : Converter<ByteArray, Blob> {
|
||||||
|
|
||||||
|
override fun getMappedType(): Class<ByteArray> = ByteArray::class.java
|
||||||
|
|
||||||
|
override fun getPersistedType(): Class<Blob> = Blob::class.java
|
||||||
|
|
||||||
|
/**
|
||||||
|
* creates BLOB(INT.MAX) = 2 GB
|
||||||
|
*/
|
||||||
|
override fun getPersistedSize(): Int? = null
|
||||||
|
|
||||||
|
override fun convertToPersisted(value: ByteArray?): Blob? {
|
||||||
|
return value?.let { SerialBlob(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToMapped(type: Class<out ByteArray>?, value: Blob?): ByteArray? {
|
||||||
|
return value?.getBytes(1, value.length().toInt())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,30 @@
|
|||||||
|
package net.corda.core.schemas.requery.converters
|
||||||
|
|
||||||
|
import io.requery.Converter
|
||||||
|
|
||||||
|
import java.sql.*
|
||||||
|
import java.time.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts from a [Instant] to a [java.sql.Timestamp] for Java 8. Note that
|
||||||
|
* when converting between the time type and the database type all times will be converted to the
|
||||||
|
* UTC zone offset.
|
||||||
|
*/
|
||||||
|
class InstantConverter : Converter<Instant, Timestamp> {
|
||||||
|
|
||||||
|
override fun getMappedType(): Class<Instant> { return Instant::class.java }
|
||||||
|
|
||||||
|
override fun getPersistedType(): Class<Timestamp> { return Timestamp::class.java }
|
||||||
|
|
||||||
|
override fun getPersistedSize(): Int? { return null }
|
||||||
|
|
||||||
|
override fun convertToPersisted(value: Instant?): Timestamp? {
|
||||||
|
if (value == null) { return null }
|
||||||
|
return Timestamp.from(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToMapped(type: Class<out Instant>, value: Timestamp?): Instant? {
|
||||||
|
if (value == null) { return null }
|
||||||
|
return value.toInstant()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.core.schemas.requery.converters
|
||||||
|
|
||||||
|
import io.requery.Converter
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert from a [SecureHash] to a [String]
|
||||||
|
*/
|
||||||
|
class SecureHashConverter : Converter<SecureHash, String> {
|
||||||
|
|
||||||
|
override fun getMappedType(): Class<SecureHash> = SecureHash::class.java
|
||||||
|
|
||||||
|
override fun getPersistedType(): Class<String> = String::class.java
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SecureHash consists of 32 bytes which need VARCHAR(64) in hex
|
||||||
|
* TODO: think about other hash widths
|
||||||
|
*/
|
||||||
|
override fun getPersistedSize(): Int? = 64
|
||||||
|
|
||||||
|
override fun convertToPersisted(value: SecureHash?): String? {
|
||||||
|
return value?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToMapped(type: Class<out SecureHash>, value: String?): SecureHash? {
|
||||||
|
return value?.let { SecureHash.parse(value) }
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,28 @@
|
|||||||
|
package net.corda.core.schemas.requery.converters
|
||||||
|
|
||||||
|
import io.requery.Converter
|
||||||
|
import net.corda.core.contracts.StateRef
|
||||||
|
import net.corda.core.crypto.SecureHash
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts from a [StateRef] to a Composite Key defined by a [String] txnHash and an [Int] index
|
||||||
|
*/
|
||||||
|
class StateRefConverter : Converter<StateRef, Pair<String, Int>> {
|
||||||
|
|
||||||
|
override fun getMappedType(): Class<StateRef> { return StateRef::class.java }
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun getPersistedType(): Class<Pair<String,Int>> { return Pair::class.java as Class<Pair<String,Int>> }
|
||||||
|
|
||||||
|
override fun getPersistedSize(): Int? { return null }
|
||||||
|
|
||||||
|
override fun convertToPersisted(value: StateRef?): Pair<String,Int>? {
|
||||||
|
if (value == null) { return null }
|
||||||
|
return Pair(value.txhash.toString(), value.index)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convertToMapped(type: Class<out StateRef>, value: Pair<String,Int>?): StateRef? {
|
||||||
|
if (value == null) { return null }
|
||||||
|
return StateRef(SecureHash.parse(value.first), value.second)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package net.corda.core.schemas.requery.converters
|
||||||
|
|
||||||
|
import io.requery.Converter
|
||||||
|
import io.requery.converter.EnumOrdinalConverter
|
||||||
|
import io.requery.sql.Mapping
|
||||||
|
import net.corda.core.contracts.ContractState
|
||||||
|
import net.corda.core.node.services.Vault
|
||||||
|
|
||||||
|
import java.sql.*
|
||||||
|
import java.time.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converter which persists a [Vault.StateStatus] enum using its enum ordinal representation
|
||||||
|
*/
|
||||||
|
class VaultStateStatusConverter() : EnumOrdinalConverter<Vault.StateStatus>(Vault.StateStatus::class.java)
|
@ -9,6 +9,7 @@ import java.util.*
|
|||||||
* In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such
|
* In an ideal JVM this would be a value type and be completely overhead free. Project Valhalla is adding such
|
||||||
* functionality to Java, but it won't arrive for a few years yet!
|
* functionality to Java, but it won't arrive for a few years yet!
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
open class OpaqueBytes(val bytes: ByteArray) {
|
open class OpaqueBytes(val bytes: ByteArray) {
|
||||||
init {
|
init {
|
||||||
check(bytes.isNotEmpty())
|
check(bytes.isNotEmpty())
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.*
|
||||||
|
import com.esotericsoftware.kryo.util.DefaultClassResolver
|
||||||
|
import com.esotericsoftware.kryo.util.Util
|
||||||
|
import net.corda.core.node.AttachmentsClassLoader
|
||||||
|
import net.corda.core.utilities.loggerFor
|
||||||
|
import java.io.PrintWriter
|
||||||
|
import java.lang.reflect.Modifier
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import java.nio.file.StandardOpenOption
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
fun Kryo.addToWhitelist(type: Class<*>) {
|
||||||
|
((classResolver as? CordaClassResolver)?.whitelist as? MutableClassWhitelist)?.add(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeStandardClassResolver(): ClassResolver {
|
||||||
|
return CordaClassResolver(GlobalTransientClassWhiteList(BuiltInExceptionsWhitelist()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun makeNoWhitelistClassResolver(): ClassResolver {
|
||||||
|
return CordaClassResolver(AllWhitelist)
|
||||||
|
}
|
||||||
|
|
||||||
|
class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() {
|
||||||
|
companion object {
|
||||||
|
private val logger = loggerFor<CordaClassResolver>()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns the registration for the specified class, or null if the class is not registered. */
|
||||||
|
override fun getRegistration(type: Class<*>): Registration? {
|
||||||
|
return super.getRegistration(type) ?: checkClass(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var whitelistEnabled = true
|
||||||
|
|
||||||
|
fun disableWhitelist() {
|
||||||
|
whitelistEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun enableWhitelist() {
|
||||||
|
whitelistEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkClass(type: Class<*>): Registration? {
|
||||||
|
/** If call path has disabled whitelisting (see [CordaKryo.register]), just return without checking. */
|
||||||
|
if(!whitelistEnabled) return null
|
||||||
|
// Allow primitives, abstracts and interfaces
|
||||||
|
if (type.isPrimitive || type == Any::class.java || Modifier.isAbstract(type.modifiers) || type==String::class.java) return null
|
||||||
|
// If array, recurse on element type
|
||||||
|
if (type.isArray) {
|
||||||
|
return checkClass(type.componentType)
|
||||||
|
}
|
||||||
|
if (!type.isEnum && Enum::class.java.isAssignableFrom(type)) {
|
||||||
|
// Specialised enum entry, so just resolve the parent Enum type since cannot annotate the specialised entry.
|
||||||
|
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)) {
|
||||||
|
throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization")
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerImplicit(type: Class<*>): Registration {
|
||||||
|
// We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent.
|
||||||
|
val references = kryo.references
|
||||||
|
try {
|
||||||
|
kryo.references = true
|
||||||
|
return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt()))
|
||||||
|
} finally {
|
||||||
|
kryo.references = references
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't allow the annotation for classes in attachments for now. The class will be on the main classpath if we have the CorDapp installed.
|
||||||
|
// We also do not allow extension of KryoSerializable for annotated classes, or combination with @DefaultSerializer for custom serialisation.
|
||||||
|
// TODO: Later we can support annotations on attachment classes and spin up a proxy via bytecode that we know is harmless.
|
||||||
|
private fun checkForAnnotation(type: Class<*>): Boolean {
|
||||||
|
return (type.classLoader !is AttachmentsClassLoader)
|
||||||
|
&& !KryoSerializable::class.java.isAssignableFrom(type)
|
||||||
|
&& !type.isAnnotationPresent(DefaultSerializer::class.java)
|
||||||
|
&& (type.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(type))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively check interfaces for our annotation.
|
||||||
|
private fun hasAnnotationOnInterface(type: Class<*>): Boolean {
|
||||||
|
return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(it) }
|
||||||
|
|| (type.superclass != null && hasAnnotationOnInterface(type.superclass))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ClassWhitelist {
|
||||||
|
fun hasListed(type: Class<*>): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MutableClassWhitelist : ClassWhitelist {
|
||||||
|
fun add(entry: Class<*>)
|
||||||
|
}
|
||||||
|
|
||||||
|
object EmptyWhitelist : ClassWhitelist {
|
||||||
|
override fun hasListed(type: Class<*>): Boolean = false
|
||||||
|
}
|
||||||
|
|
||||||
|
class BuiltInExceptionsWhitelist : ClassWhitelist {
|
||||||
|
override fun hasListed(type: Class<*>): Boolean = Throwable::class.java.isAssignableFrom(type) && type.`package`.name.startsWith("java.")
|
||||||
|
}
|
||||||
|
|
||||||
|
object AllWhitelist : ClassWhitelist {
|
||||||
|
override fun hasListed(type: Class<*>): Boolean = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Need some concept of from which class loader
|
||||||
|
class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClassWhitelist, ClassWhitelist by delegate {
|
||||||
|
companion object {
|
||||||
|
val whitelist: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hasListed(type: Class<*>): Boolean {
|
||||||
|
return (type.name in whitelist) || delegate.hasListed(type)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(entry: Class<*>) {
|
||||||
|
whitelist += entry.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class is not currently used, but can be installed to log a large number of missing entries from the whitelist
|
||||||
|
* and was used to track down the initial set.
|
||||||
|
*/
|
||||||
|
class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist {
|
||||||
|
companion object {
|
||||||
|
val log = loggerFor<LoggingWhitelist>()
|
||||||
|
val globallySeen: MutableSet<String> = Collections.synchronizedSet(mutableSetOf())
|
||||||
|
val journalWriter: PrintWriter? = openOptionalDynamicWhitelistJournal()
|
||||||
|
|
||||||
|
private fun openOptionalDynamicWhitelistJournal(): PrintWriter? {
|
||||||
|
val fileName = System.getenv("WHITELIST_FILE")
|
||||||
|
if (fileName != null && fileName.isNotEmpty()) {
|
||||||
|
try {
|
||||||
|
return PrintWriter(Files.newBufferedWriter(Paths.get(fileName), StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE), true)
|
||||||
|
} catch(ioEx: Exception) {
|
||||||
|
log.error("Could not open/create whitelist journal file for append: $fileName", ioEx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val locallySeen: MutableSet<String> = mutableSetOf()
|
||||||
|
private val alreadySeen: MutableSet<String> get() = if (global) globallySeen else locallySeen
|
||||||
|
|
||||||
|
override fun hasListed(type: Class<*>): Boolean {
|
||||||
|
if (type.name !in alreadySeen && !delegate.hasListed(type)) {
|
||||||
|
alreadySeen += type.name
|
||||||
|
val className = Util.className(type)
|
||||||
|
log.warn("Dynamically whitelisted class $className")
|
||||||
|
if (journalWriter != null) {
|
||||||
|
journalWriter.println(className)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun add(entry: Class<*>) {
|
||||||
|
if (delegate is MutableClassWhitelist) {
|
||||||
|
delegate.add(entry)
|
||||||
|
} else {
|
||||||
|
throw UnsupportedOperationException("Cannot add to whitelist since delegate whitelist is not mutable.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
|||||||
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import java.lang.annotation.Inherited
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This annotation is a marker to indicate that a class is permitted and intended to be serialized as part of Node messaging.
|
||||||
|
*
|
||||||
|
* Strictly speaking, it is critical to identifying that a class is intended to be deserialized by the node, to avoid
|
||||||
|
* a security compromise later when a vulnerability is discovered in the deserialisation of a class that just happens to
|
||||||
|
* be on the classpath, perhaps from a 3rd party library, as has been witnessed elsewhere.
|
||||||
|
*
|
||||||
|
* It also makes it possible for a code reviewer to clearly identify the classes that can be passed on the wire.
|
||||||
|
*
|
||||||
|
* TODO: As we approach a long term wire format, this annotation will only be permitted on classes that meet certain criteria.
|
||||||
|
*/
|
||||||
|
@Target(AnnotationTarget.CLASS)
|
||||||
|
@Retention(AnnotationRetention.RUNTIME)
|
||||||
|
@Inherited
|
||||||
|
annotation class CordaSerializable
|
@ -0,0 +1,85 @@
|
|||||||
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.Kryo
|
||||||
|
import com.esotericsoftware.kryo.serializers.CompatibleFieldSerializer
|
||||||
|
import com.esotericsoftware.kryo.serializers.FieldSerializer
|
||||||
|
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||||
|
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||||
|
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||||
|
import de.javakaffee.kryoserializers.guava.*
|
||||||
|
import net.corda.core.crypto.CompositeKey
|
||||||
|
import net.corda.core.crypto.MetaData
|
||||||
|
import net.corda.core.node.CordaPluginRegistry
|
||||||
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
import net.corda.core.utilities.NonEmptySet
|
||||||
|
import net.corda.core.utilities.NonEmptySetSerializer
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
|
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||||
|
import java.io.BufferedInputStream
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
object DefaultKryoCustomizer {
|
||||||
|
private val pluginRegistries: List<CordaPluginRegistry> by lazy {
|
||||||
|
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
|
||||||
|
val unusedKryo = Kryo(makeStandardClassResolver(), MapReferenceResolver())
|
||||||
|
val customization = KryoSerializationCustomization(unusedKryo)
|
||||||
|
ServiceLoader.load(CordaPluginRegistry::class.java).toList().filter { it.customizeSerialization(customization) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun customize(kryo: Kryo): Kryo {
|
||||||
|
return kryo.apply {
|
||||||
|
// Store a little schema of field names in the stream the first time a class is used which increases tolerance
|
||||||
|
// for change to a class.
|
||||||
|
setDefaultSerializer(CompatibleFieldSerializer::class.java)
|
||||||
|
// Take the safest route here and allow subclasses to have fields named the same as super classes.
|
||||||
|
fieldSerializerConfig.setCachedFieldNameStrategy(FieldSerializer.CachedFieldNameStrategy.EXTENDED)
|
||||||
|
|
||||||
|
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
|
||||||
|
// no-arg constructor available.
|
||||||
|
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
||||||
|
|
||||||
|
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
|
||||||
|
register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class))
|
||||||
|
register(WireTransaction::class.java, WireTransactionSerializer)
|
||||||
|
register(SerializedBytes::class.java, SerializedBytesSerializer)
|
||||||
|
|
||||||
|
UnmodifiableCollectionsSerializer.registerSerializers(this)
|
||||||
|
ImmutableListSerializer.registerSerializers(this)
|
||||||
|
ImmutableSetSerializer.registerSerializers(this)
|
||||||
|
ImmutableSortedSetSerializer.registerSerializers(this)
|
||||||
|
ImmutableMapSerializer.registerSerializers(this)
|
||||||
|
ImmutableMultimapSerializer.registerSerializers(this)
|
||||||
|
|
||||||
|
register(BufferedInputStream::class.java, InputStreamSerializer)
|
||||||
|
register(Class.forName("sun.net.www.protocol.jar.JarURLConnection\$JarURLInputStream"), InputStreamSerializer)
|
||||||
|
|
||||||
|
noReferencesWithin<WireTransaction>()
|
||||||
|
|
||||||
|
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
|
||||||
|
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
|
||||||
|
|
||||||
|
// Using a custom serializer for compactness
|
||||||
|
register(CompositeKey.Node::class.java, CompositeKeyNodeSerializer)
|
||||||
|
register(CompositeKey.Leaf::class.java, CompositeKeyLeafSerializer)
|
||||||
|
|
||||||
|
// Exceptions. We don't bother sending the stack traces as the client will fill in its own anyway.
|
||||||
|
register(Array<StackTraceElement>::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, obj -> })
|
||||||
|
|
||||||
|
// This ensures a NonEmptySetSerializer is constructed with an initial value.
|
||||||
|
register(NonEmptySet::class.java, NonEmptySetSerializer)
|
||||||
|
|
||||||
|
/** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */
|
||||||
|
addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer)
|
||||||
|
|
||||||
|
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
|
||||||
|
|
||||||
|
register(MetaData::class.java, MetaDataSerializer)
|
||||||
|
register(BitSet::class.java, ReferencesAwareJavaSerializer)
|
||||||
|
|
||||||
|
val customization = KryoSerializationCustomization(this)
|
||||||
|
pluginRegistries.forEach { it.customizeSerialization(customization) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,37 +1,26 @@
|
|||||||
package net.corda.core.serialization
|
package net.corda.core.serialization
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Fiber
|
import com.esotericsoftware.kryo.*
|
||||||
import co.paralleluniverse.io.serialization.kryo.KryoSerializer
|
|
||||||
import com.esotericsoftware.kryo.Kryo
|
|
||||||
import com.esotericsoftware.kryo.Kryo.DefaultInstantiatorStrategy
|
|
||||||
import com.esotericsoftware.kryo.KryoException
|
|
||||||
import com.esotericsoftware.kryo.Registration
|
|
||||||
import com.esotericsoftware.kryo.Serializer
|
|
||||||
import com.esotericsoftware.kryo.io.Input
|
import com.esotericsoftware.kryo.io.Input
|
||||||
import com.esotericsoftware.kryo.io.Output
|
import com.esotericsoftware.kryo.io.Output
|
||||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||||
import com.esotericsoftware.kryo.serializers.MapSerializer
|
import com.esotericsoftware.kryo.serializers.MapSerializer
|
||||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
|
||||||
import de.javakaffee.kryoserializers.guava.*
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.node.AttachmentsClassLoader
|
import net.corda.core.node.AttachmentsClassLoader
|
||||||
import net.corda.core.node.services.AttachmentStorage
|
import net.corda.core.node.services.AttachmentStorage
|
||||||
import net.corda.core.transactions.SignedTransaction
|
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.NonEmptySet
|
|
||||||
import net.corda.core.utilities.NonEmptySetSerializer
|
|
||||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
|
||||||
import java.io.*
|
import java.io.*
|
||||||
import java.lang.reflect.InvocationTargetException
|
import java.lang.reflect.InvocationTargetException
|
||||||
import java.nio.file.Files
|
import java.nio.file.Files
|
||||||
import java.nio.file.Path
|
import java.nio.file.Path
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
import java.security.spec.InvalidKeySpecException
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import javax.annotation.concurrent.ThreadSafe
|
import javax.annotation.concurrent.ThreadSafe
|
||||||
@ -64,37 +53,55 @@ import kotlin.reflect.jvm.javaType
|
|||||||
* in invalid states, thus violating system invariants. It isn't designed to handle malicious streams and therefore,
|
* in invalid states, thus violating system invariants. It isn't designed to handle malicious streams and therefore,
|
||||||
* isn't usable beyond the prototyping stage. But that's fine: we can revisit serialisation technologies later after
|
* isn't usable beyond the prototyping stage. But that's fine: we can revisit serialisation technologies later after
|
||||||
* a formal evaluation process.
|
* a formal evaluation process.
|
||||||
|
*
|
||||||
|
* We now distinguish between internal, storage related Kryo and external, network facing Kryo. We presently use
|
||||||
|
* some non-whitelisted classes as part of internal storage.
|
||||||
|
* TODO: eliminate internal, storage related whitelist issues, such as private keys in blob storage.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// A convenient instance of Kryo pre-configured with some useful things. Used as a default by various functions.
|
// A convenient instance of Kryo pre-configured with some useful things. Used as a default by various functions.
|
||||||
val THREAD_LOCAL_KRYO: ThreadLocal<Kryo> = ThreadLocal.withInitial { createKryo() }
|
private val THREAD_LOCAL_KRYO: ThreadLocal<Kryo> = ThreadLocal.withInitial { createKryo() }
|
||||||
|
// Same again, but this has whitelisting turned off for internal storage use only.
|
||||||
|
private val INTERNAL_THREAD_LOCAL_KRYO: ThreadLocal<Kryo> = ThreadLocal.withInitial { createInternalKryo() }
|
||||||
|
|
||||||
|
fun threadLocalP2PKryo(): Kryo = THREAD_LOCAL_KRYO.get()
|
||||||
|
fun threadLocalStorageKryo(): Kryo = INTERNAL_THREAD_LOCAL_KRYO.get()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize]
|
* A type safe wrapper around a byte array that contains a serialised object. You can call [SerializedBytes.deserialize]
|
||||||
* to get the original object back.
|
* to get the original object back.
|
||||||
*/
|
*/
|
||||||
class SerializedBytes<T : Any>(bytes: ByteArray) : OpaqueBytes(bytes) {
|
class SerializedBytes<T : Any>(bytes: ByteArray, val internalOnly: Boolean = false) : OpaqueBytes(bytes) {
|
||||||
// It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer.
|
// It's OK to use lazy here because SerializedBytes is configured to use the ImmutableClassSerializer.
|
||||||
val hash: SecureHash by lazy { bytes.sha256() }
|
val hash: SecureHash by lazy { bytes.sha256() }
|
||||||
|
|
||||||
fun writeToFile(path: Path): Path = Files.write(path, bytes)
|
fun writeToFile(path: Path): Path = Files.write(path, bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// "corda" + majorVersionByte + minorVersionMSB + minorVersionLSB
|
||||||
|
private val KryoHeaderV0_1: OpaqueBytes = OpaqueBytes("corda\u0000\u0000\u0001".toByteArray())
|
||||||
|
|
||||||
// Some extension functions that make deserialisation convenient and provide auto-casting of the result.
|
// Some extension functions that make deserialisation convenient and provide auto-casting of the result.
|
||||||
fun <T : Any> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T {
|
fun <T : Any> ByteArray.deserialize(kryo: Kryo = threadLocalP2PKryo()): T {
|
||||||
|
Input(this).use {
|
||||||
|
val header = OpaqueBytes(it.readBytes(8))
|
||||||
|
if (header != KryoHeaderV0_1) {
|
||||||
|
throw KryoException("Serialized bytes header does not match any known format.")
|
||||||
|
}
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
return kryo.readClassAndObject(Input(this)) as T
|
return kryo.readClassAndObject(it) as T
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun <T : Any> OpaqueBytes.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T {
|
fun <T : Any> OpaqueBytes.deserialize(kryo: Kryo = threadLocalP2PKryo()): T {
|
||||||
return this.bytes.deserialize(kryo)
|
return this.bytes.deserialize(kryo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The more specific deserialize version results in the bytes being cached, which is faster.
|
// The more specific deserialize version results in the bytes being cached, which is faster.
|
||||||
@JvmName("SerializedBytesWireTransaction")
|
@JvmName("SerializedBytesWireTransaction")
|
||||||
fun SerializedBytes<WireTransaction>.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction = WireTransaction.deserialize(this, kryo)
|
fun SerializedBytes<WireTransaction>.deserialize(kryo: Kryo = threadLocalP2PKryo()): WireTransaction = WireTransaction.deserialize(this, kryo)
|
||||||
|
|
||||||
fun <T : Any> SerializedBytes<T>.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T = bytes.deserialize(kryo)
|
fun <T : Any> SerializedBytes<T>.deserialize(kryo: Kryo = if (internalOnly) threadLocalStorageKryo() else threadLocalP2PKryo()): T = bytes.deserialize(kryo)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A serialiser that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure
|
* A serialiser that avoids writing the wrapper class to the byte stream, thus ensuring [SerializedBytes] is a pure
|
||||||
@ -115,12 +122,13 @@ object SerializedBytesSerializer : Serializer<SerializedBytes<Any>>() {
|
|||||||
* Can be called on any object to convert it to a byte array (wrapped by [SerializedBytes]), regardless of whether
|
* Can be called on any object to convert it to a byte array (wrapped by [SerializedBytes]), regardless of whether
|
||||||
* the type is marked as serializable or was designed for it (so be careful!).
|
* the type is marked as serializable or was designed for it (so be careful!).
|
||||||
*/
|
*/
|
||||||
fun <T : Any> T.serialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): SerializedBytes<T> {
|
fun <T : Any> T.serialize(kryo: Kryo = threadLocalP2PKryo(), internalOnly: Boolean = false): SerializedBytes<T> {
|
||||||
val stream = ByteArrayOutputStream()
|
val stream = ByteArrayOutputStream()
|
||||||
Output(stream).use {
|
Output(stream).use {
|
||||||
|
it.writeBytes(KryoHeaderV0_1.bytes)
|
||||||
kryo.writeClassAndObject(it, this)
|
kryo.writeClassAndObject(it, this)
|
||||||
}
|
}
|
||||||
return SerializedBytes(stream.toByteArray())
|
return SerializedBytes(stream.toByteArray(), internalOnly)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -261,6 +269,7 @@ fun Input.readBytesWithLength(): ByteArray {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found */
|
/** Thrown during deserialisation to indicate that an attachment needed to construct the [WireTransaction] is not found */
|
||||||
|
@CordaSerializable
|
||||||
class MissingAttachmentsException(val ids: List<SecureHash>) : Exception()
|
class MissingAttachmentsException(val ids: List<SecureHash>) : Exception()
|
||||||
|
|
||||||
/** A serialisation engine that knows how to deserialise code inside a sandbox */
|
/** A serialisation engine that knows how to deserialise code inside a sandbox */
|
||||||
@ -389,65 +398,55 @@ object KotlinObjectSerializer : Serializer<DeserializeAsKotlinObjectDef>() {
|
|||||||
override fun write(kryo: Kryo, output: Output, obj: DeserializeAsKotlinObjectDef) {}
|
override fun write(kryo: Kryo, output: Output, obj: DeserializeAsKotlinObjectDef) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createKryo(k: Kryo = Kryo()): Kryo {
|
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
|
||||||
return k.apply {
|
fun createInternalKryo(k: Kryo = CordaKryo(makeNoWhitelistClassResolver())): Kryo {
|
||||||
// Allow any class to be deserialized (this is insecure but for prototyping we don't care)
|
return DefaultKryoCustomizer.customize(k)
|
||||||
isRegistrationRequired = false
|
}
|
||||||
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
|
|
||||||
// no-arg constructor available.
|
|
||||||
instantiatorStrategy = DefaultInstantiatorStrategy(StdInstantiatorStrategy())
|
|
||||||
|
|
||||||
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
|
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
|
||||||
|
fun createKryo(k: Kryo = CordaKryo(makeStandardClassResolver())): Kryo {
|
||||||
|
return DefaultKryoCustomizer.customize(k)
|
||||||
|
}
|
||||||
|
|
||||||
// Because we like to stick a Kryo object in a ThreadLocal to speed things up a bit, we can end up trying to
|
/**
|
||||||
// serialise the Kryo object itself when suspending a fiber. That's dumb, useless AND can cause crashes, so
|
* We need to disable whitelist checking during calls from our Kryo code to register a serializer, since it checks
|
||||||
// we avoid it here.
|
* for existing registrations and then will enter our [CordaClassResolver.getRegistration] method.
|
||||||
register(Kryo::class,
|
*/
|
||||||
read = { kryo, input -> createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo) },
|
open class CordaKryo(classResolver: ClassResolver) : Kryo(classResolver, MapReferenceResolver()) {
|
||||||
write = { kryo, output, obj -> }
|
override fun register(type: Class<*>?): Registration {
|
||||||
)
|
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||||
|
try {
|
||||||
|
return super.register(type)
|
||||||
|
} finally {
|
||||||
|
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
|
override fun register(type: Class<*>?, id: Int): Registration {
|
||||||
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
|
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||||
register(Instant::class.java, ReferencesAwareJavaSerializer)
|
try {
|
||||||
|
return super.register(type, id)
|
||||||
|
} finally {
|
||||||
|
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Using a custom serializer for compactness
|
override fun register(type: Class<*>?, serializer: Serializer<*>?): Registration {
|
||||||
register(CompositeKey.Node::class.java, CompositeKeyNodeSerializer)
|
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||||
register(CompositeKey.Leaf::class.java, CompositeKeyLeafSerializer)
|
try {
|
||||||
|
return super.register(type, serializer)
|
||||||
|
} finally {
|
||||||
|
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Some classes have to be handled with the ImmutableClassSerializer because they need to have their
|
override fun register(registration: Registration?): Registration {
|
||||||
// constructors be invoked (typically for lazy members).
|
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||||
register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class))
|
try {
|
||||||
|
return super.register(registration)
|
||||||
// This class has special handling.
|
} finally {
|
||||||
register(WireTransaction::class.java, WireTransactionSerializer)
|
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||||
|
}
|
||||||
// This ensures a SerializedBytes<Foo> wrapper is written out as just a byte array.
|
|
||||||
register(SerializedBytes::class.java, SerializedBytesSerializer)
|
|
||||||
|
|
||||||
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
|
|
||||||
|
|
||||||
// This is required to make all the unit tests pass
|
|
||||||
register(Party::class.java)
|
|
||||||
|
|
||||||
// This ensures a NonEmptySetSerializer is constructed with an initial value.
|
|
||||||
register(NonEmptySet::class.java, NonEmptySetSerializer)
|
|
||||||
|
|
||||||
register(Array<StackTraceElement>::class, read = { kryo, input -> emptyArray() }, write = { kryo, output, o -> })
|
|
||||||
|
|
||||||
/** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */
|
|
||||||
addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer)
|
|
||||||
|
|
||||||
addDefaultSerializer(BufferedInputStream::class.java, InputStreamSerializer)
|
|
||||||
|
|
||||||
UnmodifiableCollectionsSerializer.registerSerializers(k)
|
|
||||||
ImmutableListSerializer.registerSerializers(k)
|
|
||||||
ImmutableSetSerializer.registerSerializers(k)
|
|
||||||
ImmutableSortedSetSerializer.registerSerializers(k)
|
|
||||||
ImmutableMapSerializer.registerSerializers(k)
|
|
||||||
ImmutableMultimapSerializer.registerSerializers(k)
|
|
||||||
|
|
||||||
noReferencesWithin<WireTransaction>()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,3 +553,32 @@ object OrderedSerializer : Serializer<HashMap<Any, Any>>() {
|
|||||||
return hm
|
return hm
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** For serialising a MetaData object. */
|
||||||
|
@ThreadSafe
|
||||||
|
object MetaDataSerializer : Serializer<MetaData>() {
|
||||||
|
override fun write(kryo: Kryo, output: Output, obj: MetaData) {
|
||||||
|
output.writeString(obj.schemeCodeName)
|
||||||
|
output.writeString(obj.versionID)
|
||||||
|
kryo.writeClassAndObject(output, obj.signatureType)
|
||||||
|
kryo.writeClassAndObject(output, obj.timestamp)
|
||||||
|
kryo.writeClassAndObject(output, obj.visibleInputs)
|
||||||
|
kryo.writeClassAndObject(output, obj.signedInputs)
|
||||||
|
output.writeBytesWithLength(obj.merkleRoot)
|
||||||
|
output.writeBytesWithLength(obj.publicKey.encoded)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
@Throws(IllegalArgumentException::class, InvalidKeySpecException::class)
|
||||||
|
override fun read(kryo: Kryo, input: Input, type: Class<MetaData>): MetaData {
|
||||||
|
val schemeCodeName = input.readString()
|
||||||
|
val versionID = input.readString()
|
||||||
|
val signatureType = kryo.readClassAndObject(input) as SignatureType
|
||||||
|
val timestamp = kryo.readClassAndObject(input) as Instant?
|
||||||
|
val visibleInputs = kryo.readClassAndObject(input) as BitSet?
|
||||||
|
val signedInputs = kryo.readClassAndObject(input) as BitSet?
|
||||||
|
val merkleRoot = input.readBytesWithLength()
|
||||||
|
val publicKey = Crypto.decodePublicKey(input.readBytesWithLength(), schemeCodeName)
|
||||||
|
return MetaData(schemeCodeName, versionID, signatureType, timestamp, visibleInputs, signedInputs, merkleRoot, publicKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -0,0 +1,13 @@
|
|||||||
|
package net.corda.core.serialization
|
||||||
|
|
||||||
|
import com.esotericsoftware.kryo.Kryo
|
||||||
|
|
||||||
|
interface SerializationCustomization {
|
||||||
|
fun addToWhitelist(type: Class<*>)
|
||||||
|
}
|
||||||
|
|
||||||
|
class KryoSerializationCustomization(val kryo: Kryo) : SerializationCustomization {
|
||||||
|
override fun addToWhitelist(type: Class<*>) {
|
||||||
|
kryo.addToWhitelist(type)
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import java.util.*
|
|||||||
*
|
*
|
||||||
* This models a similar pattern to the readReplace/writeReplace methods in Java serialization.
|
* This models a similar pattern to the readReplace/writeReplace methods in Java serialization.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
interface SerializeAsToken {
|
interface SerializeAsToken {
|
||||||
fun toToken(context: SerializeAsTokenContext): SerializationToken
|
fun toToken(context: SerializeAsTokenContext): SerializationToken
|
||||||
}
|
}
|
||||||
@ -100,6 +101,7 @@ class SerializeAsTokenContext(toBeTokenized: Any, kryo: Kryo = createKryo()) {
|
|||||||
* A class representing a [SerializationToken] for some object that is not serializable but can be looked up
|
* A class representing a [SerializationToken] for some object that is not serializable but can be looked up
|
||||||
* (when deserialized) via just the class name.
|
* (when deserialized) via just the class name.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class SingletonSerializationToken private constructor(private val className: String) : SerializationToken {
|
data class SingletonSerializationToken private constructor(private val className: String) : SerializationToken {
|
||||||
|
|
||||||
constructor(toBeTokenized: SerializeAsToken) : this(toBeTokenized.javaClass.name)
|
constructor(toBeTokenized: SerializeAsToken) : this(toBeTokenized.javaClass.name)
|
||||||
|
@ -45,6 +45,12 @@ class CompositeKeyGenerator : Generator<CompositeKey>(CompositeKey::class.java)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AnonymousPartyGenerator : Generator<AnonymousParty>(AnonymousParty::class.java) {
|
||||||
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): AnonymousParty {
|
||||||
|
return AnonymousParty(CompositeKeyGenerator().generate(random, status))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class PartyGenerator : Generator<Party>(Party::class.java) {
|
class PartyGenerator : Generator<Party>(Party::class.java) {
|
||||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Party {
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Party {
|
||||||
return Party(StringGenerator().generate(random, status), CompositeKeyGenerator().generate(random, status))
|
return Party(StringGenerator().generate(random, status), CompositeKeyGenerator().generate(random, status))
|
||||||
@ -53,7 +59,7 @@ class PartyGenerator : Generator<Party>(Party::class.java) {
|
|||||||
|
|
||||||
class PartyAndReferenceGenerator : Generator<PartyAndReference>(PartyAndReference::class.java) {
|
class PartyAndReferenceGenerator : Generator<PartyAndReference>(PartyAndReference::class.java) {
|
||||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PartyAndReference {
|
override fun generate(random: SourceOfRandomness, status: GenerationStatus): PartyAndReference {
|
||||||
return PartyAndReference(PartyGenerator().generate(random, status), OpaqueBytes(random.nextBytes(16)))
|
return PartyAndReference(AnonymousPartyGenerator().generate(random, status), OpaqueBytes(random.nextBytes(16)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,12 +44,16 @@ abstract class BaseTransaction(
|
|||||||
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
|
if (timestamp != null) check(notary != null) { "If a timestamp is provided, there must be a notary." }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun equals(other: Any?) =
|
override fun equals(other: Any?): Boolean {
|
||||||
other is BaseTransaction &&
|
if (other === this) return true
|
||||||
|
return other is BaseTransaction &&
|
||||||
notary == other.notary &&
|
notary == other.notary &&
|
||||||
mustSign == other.mustSign &&
|
mustSign == other.mustSign &&
|
||||||
type == other.type &&
|
type == other.type &&
|
||||||
timestamp == other.timestamp
|
timestamp == other.timestamp
|
||||||
|
}
|
||||||
|
|
||||||
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
|
override fun hashCode() = Objects.hash(notary, mustSign, type, timestamp)
|
||||||
|
|
||||||
|
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import net.corda.core.contracts.*
|
|||||||
import net.corda.core.crypto.CompositeKey
|
import net.corda.core.crypto.CompositeKey
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
* A LedgerTransaction is derived from a [WireTransaction]. It is the result of doing the following operations:
|
||||||
@ -16,6 +17,7 @@ import net.corda.core.crypto.SecureHash
|
|||||||
*
|
*
|
||||||
* All the above refer to inputs using a (txhash, output index) pair.
|
* All the above refer to inputs using a (txhash, output index) pair.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class LedgerTransaction(
|
class LedgerTransaction(
|
||||||
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
|
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
|
||||||
override val inputs: List<StateAndRef<*>>,
|
override val inputs: List<StateAndRef<*>>,
|
||||||
|
@ -2,11 +2,10 @@ package net.corda.core.transactions
|
|||||||
|
|
||||||
import net.corda.core.contracts.*
|
import net.corda.core.contracts.*
|
||||||
import net.corda.core.crypto.*
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.createKryo
|
import net.corda.core.serialization.createKryo
|
||||||
import net.corda.core.serialization.extendKryoHash
|
import net.corda.core.serialization.extendKryoHash
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
fun <T : Any> serializedHash(x: T): SecureHash {
|
fun <T : Any> serializedHash(x: T): SecureHash {
|
||||||
val kryo = extendKryoHash(createKryo()) // Dealing with HashMaps inside states.
|
val kryo = extendKryoHash(createKryo()) // Dealing with HashMaps inside states.
|
||||||
@ -14,9 +13,13 @@ fun <T : Any> serializedHash(x: T): SecureHash {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Interface implemented by WireTransaction and FilteredLeaves.
|
* Implemented by [WireTransaction] and [FilteredLeaves]. A TraversableTransaction allows you to iterate
|
||||||
* Property traversableList assures that we always calculate hashes in the same order, lets us define which
|
* over the flattened components of the underlying transaction structure, taking into account that some
|
||||||
* fields of WireTransaction will be included in id calculation or partial merkle tree building.
|
* may be missing in the case of this representing a "torn" transaction. Please see the user guide section
|
||||||
|
* "Transaction tear-offs" to learn more about this feature.
|
||||||
|
*
|
||||||
|
* The [availableComponents] property is used for calculation of the transaction's [MerkleTree], which is in
|
||||||
|
* turn used to derive the ID hash.
|
||||||
*/
|
*/
|
||||||
interface TraversableTransaction {
|
interface TraversableTransaction {
|
||||||
val inputs: List<StateRef>
|
val inputs: List<StateRef>
|
||||||
@ -29,30 +32,43 @@ interface TraversableTransaction {
|
|||||||
val timestamp: Timestamp?
|
val timestamp: Timestamp?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Traversing transaction fields with a list function over transaction contents. Used for leaves hashes calculation
|
* Returns a flattened list of all the components that are present in the transaction, in the following order:
|
||||||
* and user provided filtering and checking of filtered transaction.
|
*
|
||||||
|
* - Each input that is present
|
||||||
|
* - Each attachment that is present
|
||||||
|
* - Each output that is present
|
||||||
|
* - Each command that is present
|
||||||
|
* - 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
|
||||||
*/
|
*/
|
||||||
|
val availableComponents: List<Any>
|
||||||
|
get() {
|
||||||
// We may want to specify our own behaviour on certain tx fields.
|
// We may want to specify our own behaviour on certain tx fields.
|
||||||
// Like if we include them at all, what to do with null values, if we treat list as one or not etc. for building
|
// Like if we include them at all, what to do with null values, if we treat list as one or not etc. for building
|
||||||
// torn-off transaction and id calculation.
|
// torn-off transaction and id calculation.
|
||||||
val traversableList: List<Any>
|
val result = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
|
||||||
get() {
|
notary?.let { result += it }
|
||||||
val traverseList = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
|
result.addAll(mustSign)
|
||||||
if (notary != null) traverseList.add(notary!!)
|
type?.let { result += it }
|
||||||
traverseList.addAll(mustSign)
|
timestamp?.let { result += it }
|
||||||
if (type != null) traverseList.add(type!!)
|
return result
|
||||||
if (timestamp != null) traverseList.add(timestamp!!)
|
|
||||||
return traverseList
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculation of all leaves hashes that are needed for calculation of transaction id and partial Merkle branches.
|
/**
|
||||||
fun calculateLeavesHashes(): List<SecureHash> = traversableList.map { serializedHash(it) }
|
* Calculate the hashes of the sub-components of the transaction, that are used to build its Merkle tree.
|
||||||
|
* The root of the tree is the transaction identifier. The tree structure is helpful for privacy, please
|
||||||
|
* see the user-guide section "Transaction tear-offs" to learn more about this topic.
|
||||||
|
*/
|
||||||
|
val availableComponentHashes: List<SecureHash> get() = availableComponents.map { serializedHash(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every
|
* Class that holds filtered leaves for a partial Merkle transaction. We assume mixed leaf types, notice that every
|
||||||
* field from WireTransaction can be used in PartialMerkleTree calculation.
|
* field from [WireTransaction] can be used in [PartialMerkleTree] calculation.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class FilteredLeaves(
|
class FilteredLeaves(
|
||||||
override val inputs: List<StateRef>,
|
override val inputs: List<StateRef>,
|
||||||
override val attachments: List<SecureHash>,
|
override val attachments: List<SecureHash>,
|
||||||
@ -73,7 +89,7 @@ class FilteredLeaves(
|
|||||||
* @returns false if no elements were matched on a structure or checkingFun returned false.
|
* @returns false if no elements were matched on a structure or checkingFun returned false.
|
||||||
*/
|
*/
|
||||||
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
|
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
|
||||||
val checkList = traversableList.map { checkingFun(it) }
|
val checkList = availableComponents.map { checkingFun(it) }
|
||||||
return (!checkList.isEmpty()) && checkList.all { true }
|
return (!checkList.isEmpty()) && checkList.all { true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +100,7 @@ class FilteredLeaves(
|
|||||||
* @param filteredLeaves Leaves included in a filtered transaction.
|
* @param filteredLeaves Leaves included in a filtered transaction.
|
||||||
* @param partialMerkleTree Merkle branch needed to verify filteredLeaves.
|
* @param partialMerkleTree Merkle branch needed to verify filteredLeaves.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class FilteredTransaction private constructor(
|
class FilteredTransaction private constructor(
|
||||||
val rootHash: SecureHash,
|
val rootHash: SecureHash,
|
||||||
val filteredLeaves: FilteredLeaves,
|
val filteredLeaves: FilteredLeaves,
|
||||||
@ -99,8 +116,8 @@ class FilteredTransaction private constructor(
|
|||||||
filtering: (Any) -> Boolean
|
filtering: (Any) -> Boolean
|
||||||
): FilteredTransaction {
|
): FilteredTransaction {
|
||||||
val filteredLeaves = wtx.filterWithFun(filtering)
|
val filteredLeaves = wtx.filterWithFun(filtering)
|
||||||
val merkleTree = wtx.getMerkleTree()
|
val merkleTree = wtx.merkleTree
|
||||||
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.calculateLeavesHashes())
|
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.availableComponentHashes)
|
||||||
return FilteredTransaction(merkleTree.hash, filteredLeaves, pmt)
|
return FilteredTransaction(merkleTree.hash, filteredLeaves, pmt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,17 +127,9 @@ class FilteredTransaction private constructor(
|
|||||||
*/
|
*/
|
||||||
@Throws(MerkleTreeException::class)
|
@Throws(MerkleTreeException::class)
|
||||||
fun verify(): Boolean {
|
fun verify(): Boolean {
|
||||||
val hashes: List<SecureHash> = filteredLeaves.calculateLeavesHashes()
|
val hashes: List<SecureHash> = filteredLeaves.availableComponentHashes
|
||||||
if (hashes.isEmpty())
|
if (hashes.isEmpty())
|
||||||
throw MerkleTreeException("Transaction without included leaves.")
|
throw MerkleTreeException("Transaction without included leaves.")
|
||||||
return partialMerkleTree.verify(rootHash, hashes)
|
return partialMerkleTree.verify(rootHash, hashes)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs verification of Partial Merkle Branch against [rootHash]. Checks filteredLeaves with provided checkingFun.
|
|
||||||
*/
|
|
||||||
@Throws(MerkleTreeException::class)
|
|
||||||
fun verifyWithFunction(checkingFun: (Any) -> Boolean): Boolean {
|
|
||||||
return verify() && filteredLeaves.checkWithFun { checkingFun(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.DigitalSignature
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.signWithECDSA
|
import net.corda.core.crypto.signWithECDSA
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServiceHub
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
@ -23,8 +24,7 @@ import java.util.*
|
|||||||
* A transaction ID should be the hash of the [WireTransaction] Merkle tree root. Thus adding or removing a signature does not change it.
|
* A transaction ID should be the hash of the [WireTransaction] Merkle tree root. Thus adding or removing a signature does not change it.
|
||||||
*/
|
*/
|
||||||
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||||
val sigs: List<DigitalSignature.WithKey>,
|
val sigs: List<DigitalSignature.WithKey>
|
||||||
override val id: SecureHash
|
|
||||||
) : NamedByHash {
|
) : NamedByHash {
|
||||||
init {
|
init {
|
||||||
require(sigs.isNotEmpty())
|
require(sigs.isNotEmpty())
|
||||||
@ -33,12 +33,16 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
|||||||
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
|
// TODO: This needs to be reworked to ensure that the inner WireTransaction is only ever deserialised sandboxed.
|
||||||
|
|
||||||
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
||||||
val tx: WireTransaction by lazy {
|
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
|
||||||
val temp = WireTransaction.deserialize(txBits)
|
|
||||||
check(temp.id == id) { "Supplied transaction ID does not match deserialized transaction's ID - this is probably a problem in serialization/deserialization" }
|
|
||||||
temp
|
|
||||||
}
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Merkle root of the inner [WireTransaction]. Note that this is _not_ the same as the simple hash of
|
||||||
|
* [txBits], which would not use the Merkle tree structure. If the difference isn't clear, please consult
|
||||||
|
* the user guide section "Transaction tear-offs" to learn more about Merkle trees.
|
||||||
|
*/
|
||||||
|
override val id: SecureHash get() = tx.id
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class SignaturesMissingException(val missing: Set<CompositeKey>, val descriptions: List<String>, override val id: SecureHash) : NamedByHash, SignatureException() {
|
class SignaturesMissingException(val missing: Set<CompositeKey>, val descriptions: List<String>, override val id: SecureHash) : NamedByHash, SignatureException() {
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}"
|
return "Missing signatures for $descriptions on transaction ${id.prefixChars()} for ${missing.joinToString()}"
|
||||||
@ -143,4 +147,6 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
|||||||
* @return a digital signature of the transaction.
|
* @return a digital signature of the transaction.
|
||||||
*/
|
*/
|
||||||
fun signWithECDSA(keyPair: KeyPair) = keyPair.signWithECDSA(this.id.bytes)
|
fun signWithECDSA(keyPair: KeyPair) = keyPair.signWithECDSA(this.id.bytes)
|
||||||
|
|
||||||
|
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||||
}
|
}
|
||||||
|
@ -137,7 +137,7 @@ open class TransactionBuilder(
|
|||||||
throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.joinToString()}")
|
throw IllegalStateException("Missing signatures on the transaction for the public keys: ${missing.joinToString()}")
|
||||||
}
|
}
|
||||||
val wtx = toWireTransaction()
|
val wtx = toWireTransaction()
|
||||||
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs), wtx.id)
|
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs))
|
||||||
}
|
}
|
||||||
|
|
||||||
open fun addInputState(stateAndRef: StateAndRef<*>) {
|
open fun addInputState(stateAndRef: StateAndRef<*>) {
|
||||||
|
@ -7,11 +7,11 @@ import net.corda.core.crypto.MerkleTree
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.indexOfOrThrow
|
import net.corda.core.indexOfOrThrow
|
||||||
import net.corda.core.node.ServiceHub
|
import net.corda.core.node.ServicesForResolution
|
||||||
import net.corda.core.serialization.SerializedBytes
|
import net.corda.core.serialization.SerializedBytes
|
||||||
import net.corda.core.serialization.THREAD_LOCAL_KRYO
|
|
||||||
import net.corda.core.serialization.deserialize
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
|
import net.corda.core.serialization.threadLocalP2PKryo
|
||||||
import net.corda.core.utilities.Emoji
|
import net.corda.core.utilities.Emoji
|
||||||
import java.security.PublicKey
|
import java.security.PublicKey
|
||||||
|
|
||||||
@ -42,10 +42,10 @@ class WireTransaction(
|
|||||||
@Volatile @Transient private var cachedBytes: SerializedBytes<WireTransaction>? = null
|
@Volatile @Transient private var cachedBytes: SerializedBytes<WireTransaction>? = null
|
||||||
val serialized: SerializedBytes<WireTransaction> get() = cachedBytes ?: serialize().apply { cachedBytes = this }
|
val serialized: SerializedBytes<WireTransaction> get() = cachedBytes ?: serialize().apply { cachedBytes = this }
|
||||||
|
|
||||||
override val id: SecureHash by lazy { getMerkleTree().hash }
|
override val id: SecureHash by lazy { merkleTree.hash }
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun deserialize(data: SerializedBytes<WireTransaction>, kryo: Kryo = THREAD_LOCAL_KRYO.get()): WireTransaction {
|
fun deserialize(data: SerializedBytes<WireTransaction>, kryo: Kryo = threadLocalP2PKryo()): WireTransaction {
|
||||||
val wtx = data.bytes.deserialize<WireTransaction>(kryo)
|
val wtx = data.bytes.deserialize<WireTransaction>(kryo)
|
||||||
wtx.cachedBytes = data
|
wtx.cachedBytes = data
|
||||||
return wtx
|
return wtx
|
||||||
@ -70,7 +70,7 @@ class WireTransaction(
|
|||||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||||
*/
|
*/
|
||||||
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
|
||||||
fun toLedgerTransaction(services: ServiceHub): LedgerTransaction {
|
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
|
||||||
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
// Look up public keys to authenticated identities. This is just a stub placeholder and will all change in future.
|
||||||
val authenticatedArgs = commands.map {
|
val authenticatedArgs = commands.map {
|
||||||
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
||||||
@ -94,9 +94,7 @@ class WireTransaction(
|
|||||||
/**
|
/**
|
||||||
* Builds whole Merkle tree for a transaction.
|
* Builds whole Merkle tree for a transaction.
|
||||||
*/
|
*/
|
||||||
fun getMerkleTree(): MerkleTree {
|
val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(availableComponentHashes) }
|
||||||
return MerkleTree.getMerkleTree(calculateLeavesHashes())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construction of partial transaction from WireTransaction based on filtering.
|
* Construction of partial transaction from WireTransaction based on filtering.
|
||||||
@ -119,9 +117,9 @@ class WireTransaction(
|
|||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
val buf = StringBuilder()
|
val buf = StringBuilder()
|
||||||
buf.appendln("Transaction $id:")
|
buf.appendln("Transaction:")
|
||||||
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
|
for (input in inputs) buf.appendln("${Emoji.rightArrow}INPUT: $input")
|
||||||
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: $output")
|
for (output in outputs) buf.appendln("${Emoji.leftArrow}OUTPUT: ${output.data}")
|
||||||
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
|
for (command in commands) buf.appendln("${Emoji.diamond}COMMAND: $command")
|
||||||
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
|
for (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
|
||||||
return buf.toString()
|
return buf.toString()
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
package net.corda.core.utilities
|
package net.corda.core.utilities
|
||||||
|
|
||||||
|
import net.corda.core.codePointsString
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
* A simple wrapper class that contains icons and support for printing them only when we're connected to a terminal.
|
||||||
*/
|
*/
|
||||||
@ -7,15 +9,18 @@ object Emoji {
|
|||||||
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
|
// Unfortunately only Apple has a terminal that can do colour emoji AND an emoji font installed by default.
|
||||||
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
|
val hasEmojiTerminal by lazy { listOf("Apple_Terminal", "iTerm.app").contains(System.getenv("TERM_PROGRAM")) }
|
||||||
|
|
||||||
const val CODE_SANTA_CLAUS = "\ud83c\udf85"
|
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||||
const val CODE_DIAMOND = "\ud83d\udd37"
|
@JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
|
||||||
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
@JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
|
||||||
const val CODE_NEWSPAPER = "\ud83d\udcf0"
|
@JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
|
||||||
const val CODE_RIGHT_ARROW = "\u27a1\ufe0f"
|
@JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
|
||||||
const val CODE_LEFT_ARROW = "\u2b05\ufe0f"
|
@JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
|
||||||
const val CODE_GREEN_TICK = "\u2705"
|
@JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
|
||||||
const val CODE_PAPERCLIP = "\ud83d\udcce"
|
@JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
|
||||||
const val CODE_COOL_GUY = "\ud83d\ude0e"
|
@JvmStatic val CODE_COOL_GUY: String = codePointsString(0x1F60E)
|
||||||
|
@JvmStatic val CODE_NO_ENTRY: String = codePointsString(0x1F6AB)
|
||||||
|
@JvmStatic val CODE_SKULL_AND_CROSSBONES: String = codePointsString(0x2620)
|
||||||
|
@JvmStatic val CODE_BOOKS: String = codePointsString(0x1F4DA)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
|
* When non-null, toString() methods are allowed to use emoji in the output as we're going to render them to a
|
||||||
@ -31,6 +36,7 @@ object Emoji {
|
|||||||
val leftArrow: String get() = if (emojiMode.get() != null) "$CODE_LEFT_ARROW " else ""
|
val leftArrow: String get() = if (emojiMode.get() != null) "$CODE_LEFT_ARROW " else ""
|
||||||
val paperclip: String get() = if (emojiMode.get() != null) "$CODE_PAPERCLIP " else ""
|
val paperclip: String get() = if (emojiMode.get() != null) "$CODE_PAPERCLIP " else ""
|
||||||
val coolGuy: String get() = if (emojiMode.get() != null) "$CODE_COOL_GUY " else ""
|
val coolGuy: String get() = if (emojiMode.get() != null) "$CODE_COOL_GUY " else ""
|
||||||
|
val books: String get() = if (emojiMode.get() != null) "$CODE_BOOKS " else ""
|
||||||
|
|
||||||
inline fun <T> renderIfSupported(body: () -> T): T {
|
inline fun <T> renderIfSupported(body: () -> T): T {
|
||||||
emojiMode.set(this) // Could be any object.
|
emojiMode.set(this) // Could be any object.
|
||||||
@ -55,4 +61,5 @@ object Emoji {
|
|||||||
emojiMode.set(null)
|
emojiMode.set(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package net.corda.core.utilities
|
package net.corda.core.utilities
|
||||||
|
|
||||||
import net.corda.core.TransientProperty
|
import net.corda.core.TransientProperty
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.subjects.BehaviorSubject
|
import rx.subjects.BehaviorSubject
|
||||||
@ -32,7 +33,9 @@ import java.util.*
|
|||||||
* A progress tracker is *not* thread safe. You may move events from the thread making progress to another thread by
|
* A progress tracker is *not* thread safe. You may move events from the thread making progress to another thread by
|
||||||
* using the [Observable] subscribeOn call.
|
* using the [Observable] subscribeOn call.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
class ProgressTracker(vararg steps: Step) {
|
class ProgressTracker(vararg steps: Step) {
|
||||||
|
@CordaSerializable
|
||||||
sealed class Change {
|
sealed class Change {
|
||||||
class Position(val tracker: ProgressTracker, val newStep: Step) : Change() {
|
class Position(val tracker: ProgressTracker, val newStep: Step) : Change() {
|
||||||
override fun toString() = newStep.label
|
override fun toString() = newStep.label
|
||||||
@ -48,6 +51,7 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The superclass of all step objects. */
|
/** The superclass of all step objects. */
|
||||||
|
@CordaSerializable
|
||||||
open class Step(open val label: String) {
|
open class Step(open val label: String) {
|
||||||
open val changes: Observable<Change> get() = Observable.empty()
|
open val changes: Observable<Change> get() = Observable.empty()
|
||||||
open fun childProgressTracker(): ProgressTracker? = null
|
open fun childProgressTracker(): ProgressTracker? = null
|
||||||
@ -55,7 +59,7 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
|
|
||||||
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
|
/** This class makes it easier to relabel a step on the fly, to provide transient information. */
|
||||||
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
open inner class RelabelableStep(currentLabel: String) : Step(currentLabel) {
|
||||||
override val changes = BehaviorSubject.create<Change>()
|
override val changes: BehaviorSubject<Change> = BehaviorSubject.create()
|
||||||
|
|
||||||
var currentLabel: String = currentLabel
|
var currentLabel: String = currentLabel
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -81,6 +85,7 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
// This field won't be serialized.
|
// This field won't be serialized.
|
||||||
private val _changes by TransientProperty { PublishSubject.create<Change>() }
|
private val _changes by TransientProperty { PublishSubject.create<Change>() }
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
private data class Child(val tracker: ProgressTracker, @Transient val subscription: Subscription?)
|
private data class Child(val tracker: ProgressTracker, @Transient val subscription: Subscription?)
|
||||||
|
|
||||||
private val childProgressTrackers = HashMap<Step, Child>()
|
private val childProgressTrackers = HashMap<Step, Child>()
|
||||||
@ -105,8 +110,8 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
var currentStep: Step
|
var currentStep: Step
|
||||||
get() = steps[stepIndex]
|
get() = steps[stepIndex]
|
||||||
set(value) {
|
set(value) {
|
||||||
if (currentStep != value) {
|
check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" }
|
||||||
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" }
|
if (currentStep == value) return
|
||||||
|
|
||||||
val index = steps.indexOf(value)
|
val index = steps.indexOf(value)
|
||||||
require(index != -1)
|
require(index != -1)
|
||||||
@ -122,11 +127,10 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
curChangeSubscription?.unsubscribe()
|
curChangeSubscription?.unsubscribe()
|
||||||
stepIndex = index
|
stepIndex = index
|
||||||
_changes.onNext(Change.Position(this, steps[index]))
|
_changes.onNext(Change.Position(this, steps[index]))
|
||||||
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
|
curChangeSubscription = currentStep.changes.subscribe( { _changes.onNext(it) }, { _changes.onError(it) })
|
||||||
|
|
||||||
if (currentStep == DONE) _changes.onCompleted()
|
if (currentStep == DONE) _changes.onCompleted()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/** Returns the current step, descending into children to find the deepest step we are up to. */
|
/** Returns the current step, descending into children to find the deepest step we are up to. */
|
||||||
val currentStepRecursive: Step
|
val currentStepRecursive: Step
|
||||||
@ -149,6 +153,15 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
_changes.onNext(Change.Structural(this, step))
|
_changes.onNext(Change.Structural(this, step))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ends the progress tracker with the given error, bypassing any remaining steps. [changes] will emit the exception
|
||||||
|
* as an error.
|
||||||
|
*/
|
||||||
|
fun endWithError(error: Throwable) {
|
||||||
|
check(!hasEnded) { "Progress tracker has already ended" }
|
||||||
|
_changes.onError(error)
|
||||||
|
}
|
||||||
|
|
||||||
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
|
/** The parent of this tracker: set automatically by the parent when a tracker is added as a child */
|
||||||
var parent: ProgressTracker? = null
|
var parent: ProgressTracker? = null
|
||||||
private set
|
private set
|
||||||
@ -195,6 +208,9 @@ class ProgressTracker(vararg steps: Step) {
|
|||||||
* if a step changed its label or rendering).
|
* if a step changed its label or rendering).
|
||||||
*/
|
*/
|
||||||
val changes: Observable<Change> get() = _changes
|
val changes: Observable<Change> get() = _changes
|
||||||
|
|
||||||
|
/** Returns true if the progress tracker has ended, either by reaching the [DONE] step or prematurely with an error */
|
||||||
|
val hasEnded: Boolean get() = _changes.hasCompleted() || _changes.hasThrowable()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import net.corda.core.crypto.signWithECDSA
|
|||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.node.recordTransactions
|
import net.corda.core.node.recordTransactions
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
@ -29,6 +30,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
*
|
*
|
||||||
* @param M the type of a class representing proposed modification by the instigator.
|
* @param M the type of a class representing proposed modification by the instigator.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction)
|
data class Proposal<out M>(val stateRef: StateRef, val modification: M, val stx: SignedTransaction)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -63,7 +65,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
val me = listOf(myKey)
|
val me = listOf(myKey)
|
||||||
|
|
||||||
val signatures = if (participants == me) {
|
val signatures = if (participants == me) {
|
||||||
listOf(getNotarySignature(stx))
|
getNotarySignatures(stx)
|
||||||
} else {
|
} else {
|
||||||
collectSignatures(participants - me, stx)
|
collectSignatures(participants - me, stx)
|
||||||
}
|
}
|
||||||
@ -87,7 +89,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
|
|
||||||
val allPartySignedTx = stx + participantSignatures
|
val allPartySignedTx = stx + participantSignatures
|
||||||
|
|
||||||
val allSignatures = participantSignatures + getNotarySignature(allPartySignedTx)
|
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||||
parties.forEach { send(it, allSignatures) }
|
parties.forEach { send(it, allSignatures) }
|
||||||
|
|
||||||
return allSignatures
|
return allSignatures
|
||||||
@ -105,7 +107,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.WithKey {
|
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||||
progressTracker.currentStep = NOTARY
|
progressTracker.currentStep = NOTARY
|
||||||
try {
|
try {
|
||||||
return subFlow(NotaryFlow.Client(stx))
|
return subFlow(NotaryFlow.Client(stx))
|
||||||
@ -115,8 +117,10 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Type parameter should ideally be Unit but that prevents Java code from subclassing it (https://youtrack.jetbrains.com/issue/KT-15964).
|
||||||
|
// We use Void? instead of Unit? as that's what you'd use in Java.
|
||||||
abstract class Acceptor<in T>(val otherSide: Party,
|
abstract class Acceptor<in T>(val otherSide: Party,
|
||||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<Unit>() {
|
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<Void?>() {
|
||||||
companion object {
|
companion object {
|
||||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||||
@ -126,7 +130,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(StateReplacementException::class)
|
@Throws(StateReplacementException::class)
|
||||||
override fun call() {
|
override fun call(): Void? {
|
||||||
progressTracker.currentStep = VERIFYING
|
progressTracker.currentStep = VERIFYING
|
||||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||||
val stx: SignedTransaction = maybeProposal.unwrap {
|
val stx: SignedTransaction = maybeProposal.unwrap {
|
||||||
@ -135,6 +139,7 @@ abstract class AbstractStateReplacementFlow {
|
|||||||
it.stx
|
it.stx
|
||||||
}
|
}
|
||||||
approve(stx)
|
approve(stx)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -3,6 +3,7 @@ package net.corda.flows
|
|||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
|
|
||||||
|
|
||||||
@ -17,6 +18,7 @@ import net.corda.core.transactions.SignedTransaction
|
|||||||
*/
|
*/
|
||||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
||||||
val participants: Set<Party>) : FlowLogic<Unit>() {
|
val participants: Set<Party>) : FlowLogic<Unit>() {
|
||||||
|
@CordaSerializable
|
||||||
data class NotifyTxRequest(val tx: SignedTransaction)
|
data class NotifyTxRequest(val tx: SignedTransaction)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -30,10 +30,11 @@ object ContractUpgradeFlow {
|
|||||||
val command = commandData.value as UpgradeCommand
|
val command = commandData.value as UpgradeCommand
|
||||||
val participants: Set<CompositeKey> = input.participants.toSet()
|
val participants: Set<CompositeKey> = input.participants.toSet()
|
||||||
val keysThatSigned: Set<CompositeKey> = commandData.signers.toSet()
|
val keysThatSigned: Set<CompositeKey> = commandData.signers.toSet()
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||||
requireThat {
|
requireThat {
|
||||||
"The signing keys include all participant keys" by keysThatSigned.containsAll(participants)
|
"The signing keys include all participant keys" by keysThatSigned.containsAll(participants)
|
||||||
"Inputs state reference the legacy contract" by (input.contract.javaClass == upgradedContract.legacyContract.javaClass)
|
"Inputs state reference the legacy contract" by (input.contract.javaClass == upgradedContract.legacyContract)
|
||||||
"Outputs state reference the upgraded contract" by (output.contract.javaClass == command.upgradedContractClass)
|
"Outputs state reference the upgraded contract" by (output.contract.javaClass == command.upgradedContractClass)
|
||||||
"Output state must be an upgraded version of the input state" by (output == upgradedContract.upgrade(input))
|
"Output state must be an upgraded version of the input state" by (output == upgradedContract.upgrade(input))
|
||||||
}
|
}
|
||||||
@ -41,17 +42,17 @@ object ContractUpgradeFlow {
|
|||||||
|
|
||||||
private fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
private fun <OldState : ContractState, NewState : ContractState> assembleBareTx(
|
||||||
stateRef: StateAndRef<OldState>,
|
stateRef: StateAndRef<OldState>,
|
||||||
upgradedContractClass: Class<UpgradedContract<OldState, NewState>>
|
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||||
): TransactionBuilder {
|
): TransactionBuilder {
|
||||||
val contractUpgrade = upgradedContractClass.newInstance()
|
val contractUpgrade = upgradedContractClass.newInstance()
|
||||||
return TransactionType.General.Builder(stateRef.state.notary)
|
return TransactionType.General.Builder(stateRef.state.notary)
|
||||||
.withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(contractUpgrade.javaClass), stateRef.state.data.participants))
|
.withItems(stateRef, contractUpgrade.upgrade(stateRef.state.data), Command(UpgradeCommand(upgradedContractClass), stateRef.state.data.participants))
|
||||||
}
|
}
|
||||||
|
|
||||||
class Instigator<OldState : ContractState, NewState : ContractState>(
|
class Instigator<OldState : ContractState, out NewState : ContractState>(
|
||||||
originalState: StateAndRef<OldState>,
|
originalState: StateAndRef<OldState>,
|
||||||
newContractClass: Class<UpgradedContract<OldState, NewState>>
|
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||||
|
|
||||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
||||||
val stx = assembleBareTx(originalState, modification)
|
val stx = assembleBareTx(originalState, modification)
|
||||||
@ -61,10 +62,10 @@ object ContractUpgradeFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<UpgradedContract<ContractState, *>>>(otherSide) {
|
class Acceptor(otherSide: Party) : AbstractStateReplacementFlow.Acceptor<Class<out UpgradedContract<ContractState, *>>>(otherSide) {
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(StateReplacementException::class)
|
@Throws(StateReplacementException::class)
|
||||||
override fun verifyProposal(proposal: Proposal<Class<UpgradedContract<ContractState, *>>>) {
|
override fun verifyProposal(proposal: Proposal<Class<out UpgradedContract<ContractState, *>>>) {
|
||||||
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and verify outputs matches the proposed upgrade.
|
// Retrieve signed transaction from our side, we will apply the upgrade logic to the transaction on our side, and verify outputs matches the proposed upgrade.
|
||||||
val stx = subFlow(FetchTransactionsFlow(setOf(proposal.stateRef.txhash), otherSide)).fromDisk.singleOrNull()
|
val stx = subFlow(FetchTransactionsFlow(setOf(proposal.stateRef.txhash), otherSide)).fromDisk.singleOrNull()
|
||||||
requireNotNull(stx) { "We don't have a copy of the referenced state" }
|
requireNotNull(stx) { "We don't have a copy of the referenced state" }
|
||||||
|
@ -4,7 +4,6 @@ import net.corda.core.contracts.Attachment
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.crypto.sha256
|
import net.corda.core.crypto.sha256
|
||||||
import java.io.ByteArrayInputStream
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -16,18 +15,19 @@ class FetchAttachmentsFlow(requests: Set<SecureHash>,
|
|||||||
|
|
||||||
override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid)
|
override fun load(txid: SecureHash): Attachment? = serviceHub.storageService.attachments.openAttachment(txid)
|
||||||
|
|
||||||
override fun convert(wire: ByteArray): Attachment {
|
override fun convert(wire: ByteArray): Attachment = ByteArrayAttachment(wire)
|
||||||
return object : Attachment {
|
|
||||||
override fun open(): InputStream = ByteArrayInputStream(wire)
|
|
||||||
override val id: SecureHash = wire.sha256()
|
|
||||||
override fun equals(other: Any?) = (other is Attachment) && other.id == id
|
|
||||||
override fun hashCode(): Int = id.hashCode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
|
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
|
||||||
for (attachment in downloaded) {
|
for (attachment in downloaded) {
|
||||||
serviceHub.storageService.attachments.importAttachment(attachment.open())
|
serviceHub.storageService.attachments.importAttachment(attachment.open())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class ByteArrayAttachment(private val wire : ByteArray) : Attachment {
|
||||||
|
override val id: SecureHash by lazy { wire.sha256() }
|
||||||
|
override fun open(): InputStream = wire.inputStream()
|
||||||
|
override fun equals(other: Any?) = other === this || other is Attachment && other.id == this.id
|
||||||
|
override fun hashCode(): Int = id.hashCode()
|
||||||
|
override fun toString(): String = "${javaClass.simpleName}(id=$id)"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.Party
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.utilities.UntrustworthyData
|
import net.corda.core.utilities.UntrustworthyData
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
import net.corda.flows.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
import net.corda.flows.FetchDataFlow.DownloadedVsRequestedDataMismatch
|
||||||
@ -32,11 +33,18 @@ abstract class FetchDataFlow<T : NamedByHash, in W : Any>(
|
|||||||
protected val requests: Set<SecureHash>,
|
protected val requests: Set<SecureHash>,
|
||||||
protected val otherSide: Party) : FlowLogic<FetchDataFlow.Result<T>>() {
|
protected val otherSide: Party) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : IllegalArgumentException()
|
class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : IllegalArgumentException()
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class DownloadedVsRequestedSizeMismatch(val requested: Int, val got: Int) : IllegalArgumentException()
|
class DownloadedVsRequestedSizeMismatch(val requested: Int, val got: Int) : IllegalArgumentException()
|
||||||
|
|
||||||
class HashNotFound(val requested: SecureHash) : FlowException()
|
class HashNotFound(val requested: SecureHash) : FlowException()
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
data class Request(val hashes: List<SecureHash>)
|
data class Request(val hashes: List<SecureHash>)
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
|
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
|
@ -74,8 +74,8 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
return stxnsAndParties.map { pair ->
|
return stxnsAndParties.map { pair ->
|
||||||
val stx = pair.first
|
val stx = pair.first
|
||||||
val notarised = if (needsNotarySignature(stx)) {
|
val notarised = if (needsNotarySignature(stx)) {
|
||||||
val notarySig = subFlow(NotaryFlow.Client(stx))
|
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||||
stx + notarySig
|
stx + notarySignatures
|
||||||
} else {
|
} else {
|
||||||
stx
|
stx
|
||||||
}
|
}
|
||||||
@ -84,7 +84,12 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun needsNotarySignature(stx: SignedTransaction) = stx.tx.notary != null && hasNoNotarySignature(stx)
|
private fun needsNotarySignature(stx: SignedTransaction): Boolean {
|
||||||
|
val wtx = stx.tx
|
||||||
|
val needsNotarisation = wtx.inputs.isNotEmpty() || wtx.timestamp != null
|
||||||
|
return needsNotarisation && hasNoNotarySignature(stx)
|
||||||
|
|
||||||
|
}
|
||||||
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
private fun hasNoNotarySignature(stx: SignedTransaction): Boolean {
|
||||||
val notaryKey = stx.tx.notary?.owningKey
|
val notaryKey = stx.tx.notary?.owningKey
|
||||||
val signers = stx.sigs.map { it.by }.toSet()
|
val signers = stx.sigs.map { it.by }.toSet()
|
||||||
@ -117,7 +122,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
|||||||
// Load and verify each transaction.
|
// Load and verify each transaction.
|
||||||
return sorted.map { stx ->
|
return sorted.map { stx ->
|
||||||
val notary = stx.tx.notary
|
val notary = stx.tx.notary
|
||||||
// The notary signature is allowed to be missing but no others.
|
// The notary signature(s) are allowed to be missing but no others.
|
||||||
val wtx = if (notary != null) stx.verifySignatures(notary.owningKey) else stx.verifySignatures()
|
val wtx = if (notary != null) stx.verifySignatures(notary.owningKey) else stx.verifySignatures()
|
||||||
val ltx = wtx.toLedgerTransaction(augmentedLookup)
|
val ltx = wtx.toLedgerTransaction(augmentedLookup)
|
||||||
ltx.verify()
|
ltx.verify()
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
package net.corda.flows
|
||||||
|
|
||||||
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
|
import net.corda.core.crypto.Party
|
||||||
|
import net.corda.core.node.services.TimestampChecker
|
||||||
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
|
import net.corda.core.transactions.FilteredTransaction
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
|
|
||||||
|
class NonValidatingNotaryFlow(otherSide: Party,
|
||||||
|
timestampChecker: TimestampChecker,
|
||||||
|
uniquenessProvider: UniquenessProvider) : NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
|
||||||
|
/**
|
||||||
|
* The received transaction is not checked for contract-validity, as that would require fully
|
||||||
|
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
||||||
|
* history chain.
|
||||||
|
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
||||||
|
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
||||||
|
* undo the commit of the input states (the exact mechanism still needs to be worked out).
|
||||||
|
*/
|
||||||
|
@Suspendable
|
||||||
|
override fun receiveAndVerifyTx(): TransactionParts {
|
||||||
|
val ftx = receive<FilteredTransaction>(otherSide).unwrap {
|
||||||
|
it.verify()
|
||||||
|
it
|
||||||
|
}
|
||||||
|
return TransactionParts(ftx.rootHash, ftx.filteredLeaves.inputs, ftx.filteredLeaves.timestamp)
|
||||||
|
}
|
||||||
|
}
|
@ -1,31 +1,33 @@
|
|||||||
package net.corda.flows
|
package net.corda.flows
|
||||||
|
|
||||||
import co.paralleluniverse.fibers.Suspendable
|
import co.paralleluniverse.fibers.Suspendable
|
||||||
import net.corda.core.crypto.DigitalSignature
|
import net.corda.core.contracts.StateRef
|
||||||
import net.corda.core.crypto.Party
|
import net.corda.core.contracts.Timestamp
|
||||||
import net.corda.core.crypto.SignedData
|
import net.corda.core.crypto.*
|
||||||
import net.corda.core.crypto.signWithECDSA
|
|
||||||
import net.corda.core.flows.FlowException
|
import net.corda.core.flows.FlowException
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.node.services.TimestampChecker
|
import net.corda.core.node.services.TimestampChecker
|
||||||
import net.corda.core.node.services.UniquenessException
|
import net.corda.core.node.services.UniquenessException
|
||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
|
||||||
import net.corda.core.utilities.ProgressTracker
|
import net.corda.core.utilities.ProgressTracker
|
||||||
import net.corda.core.utilities.unwrap
|
import net.corda.core.utilities.unwrap
|
||||||
|
|
||||||
object NotaryFlow {
|
object NotaryFlow {
|
||||||
/**
|
/**
|
||||||
* A flow to be used for obtaining a signature from a [NotaryService] ascertaining the transaction
|
* 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.
|
* timestamp 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
|
* @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 timestamp is invalid.
|
||||||
*/
|
*/
|
||||||
open class Client(private val stx: SignedTransaction,
|
open class Client(private val stx: SignedTransaction,
|
||||||
override val progressTracker: ProgressTracker) : FlowLogic<DigitalSignature.WithKey>() {
|
override val progressTracker: ProgressTracker) : FlowLogic<List<DigitalSignature.WithKey>>() {
|
||||||
constructor(stx: SignedTransaction) : this(stx, Client.tracker())
|
constructor(stx: SignedTransaction) : this(stx, Client.tracker())
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -39,7 +41,7 @@ object NotaryFlow {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
@Throws(NotaryException::class)
|
@Throws(NotaryException::class)
|
||||||
override fun call(): DigitalSignature.WithKey {
|
override fun call(): List<DigitalSignature.WithKey> {
|
||||||
progressTracker.currentStep = REQUESTING
|
progressTracker.currentStep = REQUESTING
|
||||||
val wtx = stx.tx
|
val wtx = stx.tx
|
||||||
notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||||
@ -52,8 +54,14 @@ object NotaryFlow {
|
|||||||
throw NotaryException(NotaryError.SignaturesMissing(ex))
|
throw NotaryException(NotaryError.SignaturesMissing(ex))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val payload: Any = if (serviceHub.networkMapCache.isValidatingNotary(notaryParty)) {
|
||||||
|
stx
|
||||||
|
} else {
|
||||||
|
wtx.buildFilteredTransaction { it is StateRef || it is Timestamp }
|
||||||
|
}
|
||||||
|
|
||||||
val response = try {
|
val response = try {
|
||||||
sendAndReceive<DigitalSignature.WithKey>(notaryParty, SignRequest(stx))
|
sendAndReceive<List<DigitalSignature.WithKey>>(notaryParty, payload)
|
||||||
} catch (e: NotaryException) {
|
} catch (e: NotaryException) {
|
||||||
if (e.error is NotaryError.Conflict) {
|
if (e.error is NotaryError.Conflict) {
|
||||||
e.error.conflict.verified()
|
e.error.conflict.verified()
|
||||||
@ -61,9 +69,9 @@ object NotaryFlow {
|
|||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.unwrap { sig ->
|
return response.unwrap { signatures ->
|
||||||
validateSignature(sig, stx.id.bytes)
|
signatures.forEach { validateSignature(it, stx.id.bytes) }
|
||||||
sig
|
signatures
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,64 +81,60 @@ object NotaryFlow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks that the timestamp command is valid (if present) and commits the input state, or returns a conflict
|
* 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
|
||||||
* if any of the input states have been previously committed.
|
* if any of the input states have been previously committed.
|
||||||
*
|
*
|
||||||
* Extend this class, overriding _beforeCommit_ to add custom transaction processing/validation logic.
|
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
||||||
*
|
|
||||||
* TODO: the notary service should only be able to see timestamp commands and inputs
|
|
||||||
*/
|
*/
|
||||||
open class Service(val otherSide: Party,
|
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||||
|
abstract class Service(val otherSide: Party,
|
||||||
val timestampChecker: TimestampChecker,
|
val timestampChecker: TimestampChecker,
|
||||||
val uniquenessProvider: UniquenessProvider) : FlowLogic<Unit>() {
|
val uniquenessProvider: UniquenessProvider) : FlowLogic<Void?>() {
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun call() {
|
override fun call(): Void? {
|
||||||
val stx = receive<SignRequest>(otherSide).unwrap { it.tx }
|
val (id, inputs, timestamp) = receiveAndVerifyTx()
|
||||||
val wtx = stx.tx
|
validateTimestamp(timestamp)
|
||||||
|
commitInputStates(inputs, id)
|
||||||
validateTimestamp(wtx)
|
signAndSendResponse(id)
|
||||||
beforeCommit(stx)
|
return null
|
||||||
commitInputStates(wtx)
|
|
||||||
val sig = sign(stx.id.bytes)
|
|
||||||
send(otherSide, sig)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateTimestamp(tx: WireTransaction) {
|
|
||||||
if (tx.timestamp != null
|
|
||||||
&& !timestampChecker.isValid(tx.timestamp))
|
|
||||||
throw NotaryException(NotaryError.TimestampInvalid())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* No pre-commit processing is done. Transaction is not checked for contract-validity, as that would require fully
|
* Implement custom logic to receive the transaction to notarise, and perform verification based on validity and
|
||||||
* resolving it into a [TransactionForVerification], for which the caller would have to reveal the whole transaction
|
* privacy requirements.
|
||||||
* history chain.
|
|
||||||
* As a result, the Notary _will commit invalid transactions_ as well, but as it also records the identity of
|
|
||||||
* the caller, it is possible to raise a dispute and verify the validity of the transaction and subsequently
|
|
||||||
* undo the commit of the input states (the exact mechanism still needs to be worked out).
|
|
||||||
*/
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
open fun beforeCommit(stx: SignedTransaction) {
|
abstract fun receiveAndVerifyTx(): TransactionParts
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun signAndSendResponse(txId: SecureHash) {
|
||||||
|
val signature = sign(txId.bytes)
|
||||||
|
send(otherSide, listOf(signature))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateTimestamp(t: Timestamp?) {
|
||||||
|
if (t != null && !timestampChecker.isValid(t))
|
||||||
|
throw NotaryException(NotaryError.TimestampInvalid())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
|
* A NotaryException is thrown if any of the states have been consumed by a different transaction. Note that
|
||||||
* this method does not throw an exception when input states are present multiple times within the transaction.
|
* this method does not throw an exception when input states are present multiple times within the transaction.
|
||||||
*/
|
*/
|
||||||
private fun commitInputStates(tx: WireTransaction) {
|
private fun commitInputStates(inputs: List<StateRef>, txId: SecureHash) {
|
||||||
try {
|
try {
|
||||||
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
uniquenessProvider.commit(inputs, txId, otherSide)
|
||||||
} catch (e: UniquenessException) {
|
} catch (e: UniquenessException) {
|
||||||
val conflicts = tx.inputs.filterIndexed { i, stateRef ->
|
val conflicts = inputs.filterIndexed { i, stateRef ->
|
||||||
val consumingTx = e.error.stateHistory[stateRef]
|
val consumingTx = e.error.stateHistory[stateRef]
|
||||||
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(tx.id, i, otherSide)
|
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, otherSide)
|
||||||
}
|
}
|
||||||
if (conflicts.isNotEmpty()) {
|
if (conflicts.isNotEmpty()) {
|
||||||
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
|
||||||
throw notaryException(tx, e)
|
throw notaryException(txId, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,30 +144,35 @@ object NotaryFlow {
|
|||||||
return mySigningKey.signWithECDSA(bits)
|
return mySigningKey.signWithECDSA(bits)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun notaryException(tx: WireTransaction, e: UniquenessException): NotaryException {
|
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
|
||||||
val conflictData = e.error.serialize()
|
val conflictData = e.error.serialize()
|
||||||
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
|
||||||
return NotaryException(NotaryError.Conflict(tx, signedConflict))
|
return NotaryException(NotaryError.Conflict(txId, signedConflict))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class SignRequest(val tx: SignedTransaction)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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?)
|
||||||
|
|
||||||
class NotaryException(val error: NotaryError) : FlowException() {
|
class NotaryException(val error: NotaryError) : FlowException() {
|
||||||
override fun toString() = "${super.toString()}: Error response from Notary - $error"
|
override fun toString() = "${super.toString()}: Error response from Notary - $error"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
sealed class NotaryError {
|
sealed class NotaryError {
|
||||||
class Conflict(val tx: WireTransaction, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||||
override fun toString() = "One or more input states for transaction ${tx.id} have been used in another transaction"
|
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 */
|
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
|
||||||
class TimestampInvalid : NotaryError()
|
class TimestampInvalid : NotaryError()
|
||||||
|
|
||||||
class TransactionInvalid(val msg: String) : NotaryError()
|
class TransactionInvalid(val msg: String) : NotaryError()
|
||||||
class SignaturesInvalid(val msg: String): NotaryError()
|
class SignaturesInvalid(val msg: String) : NotaryError()
|
||||||
|
|
||||||
class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() {
|
class SignaturesMissing(val cause: SignedTransaction.SignaturesMissingException) : NotaryError() {
|
||||||
override fun toString() = cause.toString()
|
override fun toString() = cause.toString()
|
||||||
|
@ -6,6 +6,7 @@ import net.corda.core.crypto.Party
|
|||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.flows.FlowLogic
|
import net.corda.core.flows.FlowLogic
|
||||||
import net.corda.core.node.recordTransactions
|
import net.corda.core.node.recordTransactions
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.LedgerTransaction
|
import net.corda.core.transactions.LedgerTransaction
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -67,6 +68,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class ExcessivelyLargeTransactionGraph() : Exception()
|
class ExcessivelyLargeTransactionGraph() : Exception()
|
||||||
|
|
||||||
// Transactions to verify after the dependencies.
|
// Transactions to verify after the dependencies.
|
||||||
|
@ -3,10 +3,12 @@ package net.corda.flows
|
|||||||
import com.google.common.util.concurrent.ListenableFuture
|
import com.google.common.util.concurrent.ListenableFuture
|
||||||
import net.corda.core.messaging.*
|
import net.corda.core.messaging.*
|
||||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract superclass for request messages sent to services which expect a reply.
|
* Abstract superclass for request messages sent to services which expect a reply.
|
||||||
*/
|
*/
|
||||||
|
@CordaSerializable
|
||||||
interface ServiceRequestMessage {
|
interface ServiceRequestMessage {
|
||||||
val sessionID: Long
|
val sessionID: Long
|
||||||
val replyTo: SingleMessageRecipient
|
val replyTo: SingleMessageRecipient
|
||||||
|
@ -10,6 +10,7 @@ import net.corda.core.node.NodeInfo
|
|||||||
import net.corda.core.node.recordTransactions
|
import net.corda.core.node.recordTransactions
|
||||||
import net.corda.core.node.services.ServiceType
|
import net.corda.core.node.services.ServiceType
|
||||||
import net.corda.core.seconds
|
import net.corda.core.seconds
|
||||||
|
import net.corda.core.serialization.CordaSerializable
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.TransactionBuilder
|
import net.corda.core.transactions.TransactionBuilder
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
@ -31,18 +32,22 @@ import java.security.KeyPair
|
|||||||
*/
|
*/
|
||||||
object TwoPartyDealFlow {
|
object TwoPartyDealFlow {
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() {
|
class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() {
|
||||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() {
|
class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() {
|
||||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||||
}
|
}
|
||||||
|
|
||||||
// This object is serialised to the network and is the first flow message the seller sends to the buyer.
|
// This object is serialised to the network and is the first flow message the seller sends to the buyer.
|
||||||
|
@CordaSerializable
|
||||||
data class Handshake<out T>(val payload: T, val publicKey: CompositeKey)
|
data class Handshake<out T>(val payload: T, val publicKey: CompositeKey)
|
||||||
|
|
||||||
class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySig: DigitalSignature.WithKey)
|
@CordaSerializable
|
||||||
|
class SignaturesFromPrimary(val sellerSig: DigitalSignature.WithKey, val notarySigs: List<DigitalSignature.WithKey>)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [Primary] at the end sends the signed tx to all the regulator parties. This a seperate workflow which needs a
|
* [Primary] at the end sends the signed tx to all the regulator parties. This a seperate workflow which needs a
|
||||||
@ -139,9 +144,9 @@ object TwoPartyDealFlow {
|
|||||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||||
val ourSignature = computeOurSignature(stx)
|
val ourSignature = computeOurSignature(stx)
|
||||||
val allPartySignedTx = stx + ourSignature
|
val allPartySignedTx = stx + ourSignature
|
||||||
val notarySignature = getNotarySignature(allPartySignedTx)
|
val notarySignatures = getNotarySignatures(allPartySignedTx)
|
||||||
|
|
||||||
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignature)
|
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures)
|
||||||
|
|
||||||
progressTracker.currentStep = RECORDING
|
progressTracker.currentStep = RECORDING
|
||||||
|
|
||||||
@ -161,7 +166,7 @@ object TwoPartyDealFlow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.WithKey {
|
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||||
progressTracker.currentStep = NOTARY
|
progressTracker.currentStep = NOTARY
|
||||||
return subFlow(NotaryFlow.Client(stx))
|
return subFlow(NotaryFlow.Client(stx))
|
||||||
}
|
}
|
||||||
@ -173,13 +178,13 @@ object TwoPartyDealFlow {
|
|||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||||
notarySignature: DigitalSignature.WithKey): SignedTransaction {
|
notarySignatures: List<DigitalSignature.WithKey>): SignedTransaction {
|
||||||
progressTracker.currentStep = SENDING_SIGS
|
progressTracker.currentStep = SENDING_SIGS
|
||||||
val fullySigned = allPartySignedTx + notarySignature
|
val fullySigned = allPartySignedTx + notarySignatures
|
||||||
|
|
||||||
logger.trace { "Built finished transaction, sending back to other party!" }
|
logger.trace { "Built finished transaction, sending back to other party!" }
|
||||||
|
|
||||||
send(otherParty, SignaturesFromPrimary(ourSignature, notarySignature))
|
send(otherParty, SignaturesFromPrimary(ourSignature, notarySignatures))
|
||||||
return fullySigned
|
return fullySigned
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -217,7 +222,7 @@ object TwoPartyDealFlow {
|
|||||||
|
|
||||||
logger.trace { "Got signatures from other party, verifying ... " }
|
logger.trace { "Got signatures from other party, verifying ... " }
|
||||||
|
|
||||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
val fullySigned = stx + signatures.sellerSig + signatures.notarySigs
|
||||||
fullySigned.verifySignatures()
|
fullySigned.verifySignatures()
|
||||||
|
|
||||||
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
logger.trace { "Signatures received are valid. Deal transaction complete! :-)" }
|
||||||
@ -263,7 +268,7 @@ object TwoPartyDealFlow {
|
|||||||
@Suspendable protected abstract fun assembleSharedTX(handshake: Handshake<U>): Pair<TransactionBuilder, List<CompositeKey>>
|
@Suspendable protected abstract fun assembleSharedTX(handshake: Handshake<U>): Pair<TransactionBuilder, List<CompositeKey>>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@CordaSerializable
|
||||||
data class AutoOffer(val notary: Party, val dealBeingOffered: DealState)
|
data class AutoOffer(val notary: Party, val dealBeingOffered: DealState)
|
||||||
|
|
||||||
|
|
||||||
@ -300,7 +305,7 @@ object TwoPartyDealFlow {
|
|||||||
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
// And add a request for timestamping: it may be that none of the contracts need this! But it can't hurt
|
||||||
// to have one.
|
// to have one.
|
||||||
ptx.setTime(serviceHub.clock.instant(), 30.seconds)
|
ptx.setTime(serviceHub.clock.instant(), 30.seconds)
|
||||||
return Pair(ptx, arrayListOf(deal.parties.single { it.name == serviceHub.myInfo.legalIdentity.name }.owningKey))
|
return Pair(ptx, arrayListOf(deal.parties.single { it == serviceHub.myInfo.legalIdentity as AbstractParty }.owningKey))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import net.corda.core.node.services.TimestampChecker
|
|||||||
import net.corda.core.node.services.UniquenessProvider
|
import net.corda.core.node.services.UniquenessProvider
|
||||||
import net.corda.core.transactions.SignedTransaction
|
import net.corda.core.transactions.SignedTransaction
|
||||||
import net.corda.core.transactions.WireTransaction
|
import net.corda.core.transactions.WireTransaction
|
||||||
|
import net.corda.core.utilities.unwrap
|
||||||
import java.security.SignatureException
|
import java.security.SignatureException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -19,21 +20,18 @@ class ValidatingNotaryFlow(otherSide: Party,
|
|||||||
timestampChecker: TimestampChecker,
|
timestampChecker: TimestampChecker,
|
||||||
uniquenessProvider: UniquenessProvider) :
|
uniquenessProvider: UniquenessProvider) :
|
||||||
NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
|
NotaryFlow.Service(otherSide, timestampChecker, uniquenessProvider) {
|
||||||
|
/**
|
||||||
|
* The received transaction is checked for contract-validity, which requires fully resolving it into a
|
||||||
|
* [TransactionForVerification], for which the caller also has to to reveal the whole transaction
|
||||||
|
* dependency chain.
|
||||||
|
*/
|
||||||
@Suspendable
|
@Suspendable
|
||||||
override fun beforeCommit(stx: SignedTransaction) {
|
override fun receiveAndVerifyTx(): TransactionParts {
|
||||||
try {
|
val stx = receive<SignedTransaction>(otherSide).unwrap { it }
|
||||||
checkSignatures(stx)
|
checkSignatures(stx)
|
||||||
val wtx = stx.tx
|
val wtx = stx.tx
|
||||||
resolveTransaction(wtx)
|
validateTransaction(wtx)
|
||||||
wtx.toLedgerTransaction(serviceHub).verify()
|
return TransactionParts(wtx.id, wtx.inputs, wtx.timestamp)
|
||||||
} catch (e: Exception) {
|
|
||||||
when (e) {
|
|
||||||
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
|
|
||||||
is SignatureException -> throw NotaryException(NotaryError.SignaturesInvalid(e.toString()))
|
|
||||||
else -> throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkSignatures(stx: SignedTransaction) {
|
private fun checkSignatures(stx: SignedTransaction) {
|
||||||
@ -45,7 +43,19 @@ class ValidatingNotaryFlow(otherSide: Party,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suspendable
|
@Suspendable
|
||||||
private fun resolveTransaction(wtx: WireTransaction) {
|
fun validateTransaction(wtx: WireTransaction) {
|
||||||
subFlow(ResolveTransactionsFlow(wtx, otherSide))
|
try {
|
||||||
|
resolveTransaction(wtx)
|
||||||
|
wtx.toLedgerTransaction(serviceHub).verify()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw when (e) {
|
||||||
|
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
|
||||||
|
is SignatureException -> NotaryException(NotaryError.SignaturesInvalid(e.toString()))
|
||||||
|
else -> e
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suspendable
|
||||||
|
private fun resolveTransaction(wtx: WireTransaction) = subFlow(ResolveTransactionsFlow(wtx, otherSide))
|
||||||
}
|
}
|
||||||
|
@ -754,3 +754,4 @@ Nuremberg 11.05 49.45
|
|||||||
Santa Fe -60.69 -31.6
|
Santa Fe -60.69 -31.6
|
||||||
Joinville -48.84 -26.32
|
Joinville -48.84 -26.32
|
||||||
Zurich 8.55 47.36
|
Zurich 8.55 47.36
|
||||||
|
Hong Kong 114.16 22.28
|
@ -35,7 +35,7 @@ class TransactionTests {
|
|||||||
timestamp = null
|
timestamp = null
|
||||||
)
|
)
|
||||||
val bytes: SerializedBytes<WireTransaction> = wtx.serialized
|
val bytes: SerializedBytes<WireTransaction> = wtx.serialized
|
||||||
fun make(vararg keys: KeyPair) = SignedTransaction(bytes, keys.map { it.signWithECDSA(wtx.id.bytes) }, wtx.id)
|
fun make(vararg keys: KeyPair) = SignedTransaction(bytes, keys.map { it.signWithECDSA(wtx.id.bytes) })
|
||||||
assertFailsWith<IllegalArgumentException> { make().verifySignatures() }
|
assertFailsWith<IllegalArgumentException> { make().verifySignatures() }
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
|
656
core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt
Normal file
656
core/src/test/kotlin/net/corda/core/crypto/CryptoUtilsTest.kt
Normal file
@ -0,0 +1,656 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import com.google.common.collect.Sets
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAKey
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||||
|
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||||
|
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
|
||||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
|
||||||
|
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
|
||||||
|
import org.bouncycastle.jce.ECNamedCurveTable
|
||||||
|
import org.bouncycastle.jce.interfaces.ECKey
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
|
||||||
|
import org.junit.Assert.assertNotEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import java.security.KeyFactory
|
||||||
|
import java.security.Security
|
||||||
|
import java.util.*
|
||||||
|
import java.security.spec.*
|
||||||
|
import kotlin.test.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tests for cryptographic algorithms
|
||||||
|
*/
|
||||||
|
class CryptoUtilsTest {
|
||||||
|
|
||||||
|
init {
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
Security.addProvider(BouncyCastlePQCProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
val testString = "Hello World"
|
||||||
|
val testBytes = testString.toByteArray()
|
||||||
|
|
||||||
|
// key generation test
|
||||||
|
@Test
|
||||||
|
fun `Generate key pairs`() {
|
||||||
|
// testing supported algorithms
|
||||||
|
val rsaKeyPair = Crypto.generateKeyPair("RSA_SHA256")
|
||||||
|
val ecdsaKKeyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val ecdsaRKeyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
val eddsaKeyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||||
|
val sphincsKeyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512")
|
||||||
|
|
||||||
|
// not null private keys
|
||||||
|
assertNotNull(rsaKeyPair.private);
|
||||||
|
assertNotNull(ecdsaKKeyPair.private);
|
||||||
|
assertNotNull(ecdsaRKeyPair.private);
|
||||||
|
assertNotNull(eddsaKeyPair.private);
|
||||||
|
assertNotNull(sphincsKeyPair.private);
|
||||||
|
|
||||||
|
// not null public keys
|
||||||
|
assertNotNull(rsaKeyPair.public);
|
||||||
|
assertNotNull(ecdsaKKeyPair.public);
|
||||||
|
assertNotNull(ecdsaRKeyPair.public);
|
||||||
|
assertNotNull(eddsaKeyPair.public);
|
||||||
|
assertNotNull(sphincsKeyPair.public);
|
||||||
|
|
||||||
|
// fail on unsupported algorithm
|
||||||
|
try {
|
||||||
|
val wrongKeyPair = Crypto.generateKeyPair("WRONG_ALG")
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// full process tests
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RSA full process keygen-sign-verify`() {
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair("RSA_SHA256")
|
||||||
|
|
||||||
|
// test for some data
|
||||||
|
val signedData = keyPair.sign(testBytes)
|
||||||
|
val verification = keyPair.verify(signedData, testBytes)
|
||||||
|
assertTrue(verification)
|
||||||
|
|
||||||
|
// test for empty data signing
|
||||||
|
try {
|
||||||
|
keyPair.sign(ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty source data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(testBytes, ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty signed data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(ByteArray(0), testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for zero bytes data
|
||||||
|
val signedDataZeros = keyPair.sign(ByteArray(100))
|
||||||
|
val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100))
|
||||||
|
assertTrue(verificationZeros)
|
||||||
|
|
||||||
|
// test for 1MB of data (I successfully tested it locally for 1GB as well)
|
||||||
|
val MBbyte = ByteArray(1000000) // 1.000.000
|
||||||
|
Random().nextBytes(MBbyte)
|
||||||
|
val signedDataBig = keyPair.sign(MBbyte)
|
||||||
|
val verificationBig = keyPair.verify(signedDataBig, MBbyte)
|
||||||
|
assertTrue(verificationBig)
|
||||||
|
|
||||||
|
// test on malformed signatures (even if they change for 1 bit)
|
||||||
|
for (i in 0..signedData.size - 1) {
|
||||||
|
val b = signedData.get(i)
|
||||||
|
signedData.set(i,b.inc())
|
||||||
|
try {
|
||||||
|
keyPair.verify(signedData, testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
signedData.set(i,b.dec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256k1 full process keygen-sign-verify`() {
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
|
||||||
|
// test for some data
|
||||||
|
val signedData = keyPair.sign(testBytes)
|
||||||
|
val verification = keyPair.verify(signedData, testBytes)
|
||||||
|
assertTrue(verification)
|
||||||
|
|
||||||
|
// test for empty data signing
|
||||||
|
try {
|
||||||
|
keyPair.sign(ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty source data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(testBytes, ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty signed data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(ByteArray(0), testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for zero bytes data
|
||||||
|
val signedDataZeros = keyPair.sign(ByteArray(100))
|
||||||
|
val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100))
|
||||||
|
assertTrue(verificationZeros)
|
||||||
|
|
||||||
|
// test for 1MB of data (I successfully tested it locally for 1GB as well)
|
||||||
|
val MBbyte = ByteArray(1000000) // 1.000.000
|
||||||
|
Random().nextBytes(MBbyte)
|
||||||
|
val signedDataBig = keyPair.sign(MBbyte)
|
||||||
|
val verificationBig = keyPair.verify(signedDataBig, MBbyte)
|
||||||
|
assertTrue(verificationBig)
|
||||||
|
|
||||||
|
// test on malformed signatures (even if they change for 1 bit)
|
||||||
|
signedData.set(0,signedData[0].inc())
|
||||||
|
try {
|
||||||
|
keyPair.verify(signedData, testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256r1 full process keygen-sign-verify`() {
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
|
||||||
|
// test for some data
|
||||||
|
val signedData = keyPair.sign(testBytes)
|
||||||
|
val verification = keyPair.verify(signedData, testBytes)
|
||||||
|
assertTrue(verification)
|
||||||
|
|
||||||
|
// test for empty data signing
|
||||||
|
try {
|
||||||
|
keyPair.sign(ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty source data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(testBytes, ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty signed data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(ByteArray(0), testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for zero bytes data
|
||||||
|
val signedDataZeros = keyPair.sign(ByteArray(100))
|
||||||
|
val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100))
|
||||||
|
assertTrue(verificationZeros)
|
||||||
|
|
||||||
|
// test for 1MB of data (I successfully tested it locally for 1GB as well)
|
||||||
|
val MBbyte = ByteArray(1000000) // 1.000.000
|
||||||
|
Random().nextBytes(MBbyte)
|
||||||
|
val signedDataBig = keyPair.sign(MBbyte)
|
||||||
|
val verificationBig = keyPair.verify(signedDataBig, MBbyte)
|
||||||
|
assertTrue(verificationBig)
|
||||||
|
|
||||||
|
// test on malformed signatures (even if they change for 1 bit)
|
||||||
|
signedData.set(0, signedData[0].inc())
|
||||||
|
try {
|
||||||
|
keyPair.verify(signedData, testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EDDSA ed25519 full process keygen-sign-verify`() {
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||||
|
|
||||||
|
// test for some data
|
||||||
|
val signedData = keyPair.sign(testBytes)
|
||||||
|
val verification = keyPair.verify(signedData, testBytes)
|
||||||
|
assertTrue(verification)
|
||||||
|
|
||||||
|
// test for empty data signing
|
||||||
|
try {
|
||||||
|
keyPair.sign(ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty source data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(testBytes, ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty signed data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(ByteArray(0), testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for zero bytes data
|
||||||
|
val signedDataZeros = keyPair.sign(ByteArray(100))
|
||||||
|
val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100))
|
||||||
|
assertTrue(verificationZeros)
|
||||||
|
|
||||||
|
// test for 1MB of data (I successfully tested it locally for 1GB as well)
|
||||||
|
val MBbyte = ByteArray(1000000) // 1.000.000
|
||||||
|
Random().nextBytes(MBbyte)
|
||||||
|
val signedDataBig = keyPair.sign(MBbyte)
|
||||||
|
val verificationBig = keyPair.verify(signedDataBig, MBbyte)
|
||||||
|
assertTrue(verificationBig)
|
||||||
|
|
||||||
|
// test on malformed signatures (even if they change for 1 bit)
|
||||||
|
signedData.set(0, signedData[0].inc())
|
||||||
|
try {
|
||||||
|
keyPair.verify(signedData, testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SPHINCS-256 full process keygen-sign-verify`() {
|
||||||
|
|
||||||
|
val keyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512")
|
||||||
|
|
||||||
|
// test for some data
|
||||||
|
val signedData = keyPair.sign(testBytes)
|
||||||
|
val verification = keyPair.verify(signedData, testBytes)
|
||||||
|
assertTrue(verification)
|
||||||
|
|
||||||
|
// test for empty data signing
|
||||||
|
try {
|
||||||
|
keyPair.sign(ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty source data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(testBytes, ByteArray(0))
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for empty signed data when verifying
|
||||||
|
try {
|
||||||
|
keyPair.verify(ByteArray(0), testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
|
||||||
|
// test for zero bytes data
|
||||||
|
val signedDataZeros = keyPair.sign(ByteArray(100))
|
||||||
|
val verificationZeros = keyPair.verify(signedDataZeros, ByteArray(100))
|
||||||
|
assertTrue(verificationZeros)
|
||||||
|
|
||||||
|
// test for 1MB of data (I successfully tested it locally for 1GB as well)
|
||||||
|
val MBbyte = ByteArray(1000000) // 1.000.000
|
||||||
|
Random().nextBytes(MBbyte)
|
||||||
|
val signedDataBig = keyPair.sign(MBbyte)
|
||||||
|
val verificationBig = keyPair.verify(signedDataBig, MBbyte)
|
||||||
|
assertTrue(verificationBig)
|
||||||
|
|
||||||
|
// test on malformed signatures (even if they change for 1 bit)
|
||||||
|
signedData.set(0, signedData[0].inc())
|
||||||
|
try {
|
||||||
|
keyPair.verify(signedData, testBytes)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// test list of supported algorithms
|
||||||
|
@Test
|
||||||
|
fun `Check supported algorithms`() {
|
||||||
|
val algList : List<String> = Crypto.listSupportedSignatureSchemes()
|
||||||
|
val expectedAlgSet = setOf<String>("RSA_SHA256","ECDSA_SECP256K1_SHA256", "ECDSA_SECP256R1_SHA256", "EDDSA_ED25519_SHA512","SPHINCS-256_SHA512")
|
||||||
|
assertTrue { Sets.symmetricDifference(expectedAlgSet,algList.toSet()).isEmpty(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfortunately, there isn't a standard way to encode/decode keys, so we need to test per case
|
||||||
|
@Test
|
||||||
|
fun `RSA encode decode keys - required for serialization`() {
|
||||||
|
// Generate key pair.
|
||||||
|
val keyPair = Crypto.generateKeyPair("RSA_SHA256")
|
||||||
|
val (privKey, pubKey) = keyPair
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("RSA", "BC")
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKey2 = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded))
|
||||||
|
assertEquals(privKey2, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKey2 = keyFactory.generatePublic(X509EncodedKeySpec(pubKey.encoded))
|
||||||
|
assertEquals(pubKey2, pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256k1 encode decode keys - required for serialization`() {
|
||||||
|
// Generate key pair.
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privKey, pubKey) = keyPair
|
||||||
|
|
||||||
|
val kf = KeyFactory.getInstance("ECDSA", "BC")
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded))
|
||||||
|
assertEquals(privKey2, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded))
|
||||||
|
assertEquals(pubKey2, pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256r1 encode decode keys - required for serialization`() {
|
||||||
|
// Generate key pair.
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
val (privKey, pubKey) = keyPair
|
||||||
|
|
||||||
|
val kf = KeyFactory.getInstance("ECDSA", "BC")
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded))
|
||||||
|
assertEquals(privKey2, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded))
|
||||||
|
assertEquals(pubKey2, pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EdDSA encode decode keys - required for serialization`() {
|
||||||
|
// Generate key pair.
|
||||||
|
val keyPair = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||||
|
val privKey: EdDSAPrivateKey = keyPair.private as EdDSAPrivateKey
|
||||||
|
val pubKey: EdDSAPublicKey = keyPair.public as EdDSAPublicKey
|
||||||
|
|
||||||
|
val kf = EdDSAKeyFactory()
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKey2 = kf.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded))
|
||||||
|
assertEquals(privKey2, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKey2 = kf.generatePublic(X509EncodedKeySpec(pubKey.encoded))
|
||||||
|
assertEquals(pubKey2, pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SPHINCS-256 encode decode keys - required for serialization`() {
|
||||||
|
// Generate key pair.
|
||||||
|
val keyPair = Crypto.generateKeyPair("SPHINCS-256_SHA512")
|
||||||
|
val privKey: BCSphincs256PrivateKey = keyPair.private as BCSphincs256PrivateKey
|
||||||
|
val pubKey: BCSphincs256PublicKey = keyPair.public as BCSphincs256PublicKey
|
||||||
|
|
||||||
|
//1st method for encoding/decoding
|
||||||
|
|
||||||
|
val keyFactory = KeyFactory.getInstance("SPHINCS256", "BCPQC")
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKey2 = keyFactory.generatePrivate(PKCS8EncodedKeySpec(privKey.encoded))
|
||||||
|
assertEquals(privKey2, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKey2 = keyFactory.generatePublic(X509EncodedKeySpec(pubKey.encoded))
|
||||||
|
assertEquals(pubKey2, pubKey)
|
||||||
|
|
||||||
|
//2nd method for encoding/decoding
|
||||||
|
|
||||||
|
// Encode and decode private key.
|
||||||
|
val privKeyInfo : PrivateKeyInfo = PrivateKeyInfo.getInstance(privKey.encoded)
|
||||||
|
val decodedPrivKey = BCSphincs256PrivateKey(privKeyInfo)
|
||||||
|
// Check that decoded private key is equal to the initial one.
|
||||||
|
assertEquals(decodedPrivKey, privKey)
|
||||||
|
|
||||||
|
// Encode and decode public key.
|
||||||
|
val pubKeyInfo : SubjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(pubKey.encoded)
|
||||||
|
val extractedPubKey = BCSphincs256PublicKey(pubKeyInfo)
|
||||||
|
// Check that decoded private key is equal to the initial one.
|
||||||
|
assertEquals(extractedPubKey, pubKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `RSA scheme finder by key type`() {
|
||||||
|
val keyPairRSA = Crypto.generateKeyPair("RSA_SHA256")
|
||||||
|
val (privRSA, pubRSA) = keyPairRSA
|
||||||
|
assertEquals(privRSA.algorithm, "RSA")
|
||||||
|
assertEquals(pubRSA.algorithm, "RSA")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256k1 scheme finder by key type`() {
|
||||||
|
val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privK1, pubK1) = keyPairK1
|
||||||
|
|
||||||
|
// Encode and decode keys as they would be transferred.
|
||||||
|
val kf = KeyFactory.getInstance("ECDSA", "BC")
|
||||||
|
val privK1Decoded = kf.generatePrivate(PKCS8EncodedKeySpec(privK1.encoded))
|
||||||
|
val pubK1Decoded = kf.generatePublic(X509EncodedKeySpec(pubK1.encoded))
|
||||||
|
|
||||||
|
assertEquals(privK1Decoded.algorithm, "ECDSA")
|
||||||
|
assertEquals((privK1Decoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1"))
|
||||||
|
assertEquals(pubK1Decoded.algorithm, "ECDSA")
|
||||||
|
assertEquals((pubK1Decoded as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256k1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `ECDSA secp256r1 scheme finder by key type`() {
|
||||||
|
val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
val (privR1, pubR1) = keyPairR1
|
||||||
|
assertEquals(privR1.algorithm, "ECDSA")
|
||||||
|
assertEquals((privR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1"))
|
||||||
|
assertEquals(pubR1.algorithm, "ECDSA")
|
||||||
|
assertEquals((pubR1 as ECKey).parameters, ECNamedCurveTable.getParameterSpec("secp256r1"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `EdDSA scheme finder by key type`() {
|
||||||
|
val keyPairEd = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||||
|
val (privEd, pubEd) = keyPairEd
|
||||||
|
|
||||||
|
assertEquals(privEd.algorithm, "EdDSA")
|
||||||
|
assertEquals((privEd as EdDSAKey).params, EdDSANamedCurveTable.getByName("ed25519-sha-512"))
|
||||||
|
assertEquals(pubEd.algorithm, "EdDSA")
|
||||||
|
assertEquals((pubEd as EdDSAKey).params, EdDSANamedCurveTable.getByName("ed25519-sha-512"))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `SPHINCS-256 scheme finder by key type`() {
|
||||||
|
val keyPairSP = Crypto.generateKeyPair("SPHINCS-256_SHA512")
|
||||||
|
val (privSP, pubSP) = keyPairSP
|
||||||
|
assertEquals(privSP.algorithm, "SPHINCS-256")
|
||||||
|
assertEquals(pubSP.algorithm, "SPHINCS-256")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Automatic EdDSA key-type detection and decoding`() {
|
||||||
|
val keyPairEd = Crypto.generateKeyPair("EDDSA_ED25519_SHA512")
|
||||||
|
val (privEd, pubEd) = keyPairEd
|
||||||
|
val encodedPrivEd = privEd.encoded
|
||||||
|
val encodedPubEd = pubEd.encoded
|
||||||
|
|
||||||
|
val decodedPrivEd = Crypto.decodePrivateKey(encodedPrivEd)
|
||||||
|
assertEquals(decodedPrivEd.algorithm, "EdDSA")
|
||||||
|
assertEquals(decodedPrivEd, privEd)
|
||||||
|
|
||||||
|
val decodedPubEd = Crypto.decodePublicKey(encodedPubEd)
|
||||||
|
assertEquals(decodedPubEd.algorithm, "EdDSA")
|
||||||
|
assertEquals(decodedPubEd, pubEd)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Automatic ECDSA secp256k1 key-type detection and decoding`() {
|
||||||
|
val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privK1, pubK1) = keyPairK1
|
||||||
|
val encodedPrivK1 = privK1.encoded
|
||||||
|
val encodedPubK1 = pubK1.encoded
|
||||||
|
|
||||||
|
val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1)
|
||||||
|
assertEquals(decodedPrivK1.algorithm, "ECDSA")
|
||||||
|
assertEquals(decodedPrivK1, privK1)
|
||||||
|
|
||||||
|
val decodedPubK1 = Crypto.decodePublicKey(encodedPubK1)
|
||||||
|
assertEquals(decodedPubK1.algorithm, "ECDSA")
|
||||||
|
assertEquals(decodedPubK1, pubK1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Automatic ECDSA secp256r1 key-type detection and decoding`() {
|
||||||
|
val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
val (privR1, pubR1) = keyPairR1
|
||||||
|
val encodedPrivR1 = privR1.encoded
|
||||||
|
val encodedPubR1 = pubR1.encoded
|
||||||
|
|
||||||
|
val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1)
|
||||||
|
assertEquals(decodedPrivR1.algorithm, "ECDSA")
|
||||||
|
assertEquals(decodedPrivR1, privR1)
|
||||||
|
|
||||||
|
val decodedPubR1 = Crypto.decodePublicKey(encodedPubR1)
|
||||||
|
assertEquals(decodedPubR1.algorithm, "ECDSA")
|
||||||
|
assertEquals(decodedPubR1, pubR1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Automatic RSA key-type detection and decoding`() {
|
||||||
|
val keyPairRSA = Crypto.generateKeyPair("RSA_SHA256")
|
||||||
|
val (privRSA, pubRSA) = keyPairRSA
|
||||||
|
val encodedPrivRSA = privRSA.encoded
|
||||||
|
val encodedPubRSA = pubRSA.encoded
|
||||||
|
|
||||||
|
val decodedPrivRSA = Crypto.decodePrivateKey(encodedPrivRSA)
|
||||||
|
assertEquals(decodedPrivRSA.algorithm, "RSA")
|
||||||
|
assertEquals(decodedPrivRSA, privRSA)
|
||||||
|
|
||||||
|
val decodedPubRSA = Crypto.decodePublicKey(encodedPubRSA)
|
||||||
|
assertEquals(decodedPubRSA.algorithm, "RSA")
|
||||||
|
assertEquals(decodedPubRSA, pubRSA)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Automatic SPHINCS-256 key-type detection and decoding`() {
|
||||||
|
val keyPairSP = Crypto.generateKeyPair("SPHINCS-256_SHA512")
|
||||||
|
val (privSP, pubSP) = keyPairSP
|
||||||
|
val encodedPrivSP = privSP.encoded
|
||||||
|
val encodedPubSP = pubSP.encoded
|
||||||
|
|
||||||
|
val decodedPrivSP = Crypto.decodePrivateKey(encodedPrivSP)
|
||||||
|
assertEquals(decodedPrivSP.algorithm, "SPHINCS-256")
|
||||||
|
assertEquals(decodedPrivSP, privSP)
|
||||||
|
|
||||||
|
val decodedPubSP = Crypto.decodePublicKey(encodedPubSP)
|
||||||
|
assertEquals(decodedPubSP.algorithm, "SPHINCS-256")
|
||||||
|
assertEquals(decodedPubSP, pubSP)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Failure test between K1 and R1 keys`() {
|
||||||
|
val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privK1, pubK1) = keyPairK1
|
||||||
|
val encodedPrivK1 = privK1.encoded
|
||||||
|
val decodedPrivK1 = Crypto.decodePrivateKey(encodedPrivK1)
|
||||||
|
|
||||||
|
val keyPairR1 = Crypto.generateKeyPair("ECDSA_SECP256R1_SHA256")
|
||||||
|
val (privR1, pubR1) = keyPairR1
|
||||||
|
val encodedPrivR1 = privR1.encoded
|
||||||
|
val decodedPrivR1 = Crypto.decodePrivateKey(encodedPrivR1)
|
||||||
|
|
||||||
|
assertNotEquals(decodedPrivK1, decodedPrivR1)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Decoding Failure on randomdata as key`() {
|
||||||
|
val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privK1, pubK1) = keyPairK1
|
||||||
|
val encodedPrivK1 = privK1.encoded
|
||||||
|
|
||||||
|
// Test on random encoded bytes.
|
||||||
|
val fakeEncodedKey = ByteArray(encodedPrivK1.size)
|
||||||
|
val r = Random()
|
||||||
|
r.nextBytes(fakeEncodedKey)
|
||||||
|
|
||||||
|
// fail on fake key.
|
||||||
|
try {
|
||||||
|
val decodedFake = Crypto.decodePrivateKey(fakeEncodedKey)
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `Decoding Failure on malformed keys`() {
|
||||||
|
val keyPairK1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val (privK1, pubK1) = keyPairK1
|
||||||
|
val encodedPrivK1 = privK1.encoded
|
||||||
|
|
||||||
|
// fail on malformed key.
|
||||||
|
for (i in 0..encodedPrivK1.size - 1) {
|
||||||
|
val b = encodedPrivK1.get(i)
|
||||||
|
encodedPrivK1.set(i,b.inc())
|
||||||
|
try {
|
||||||
|
val decodedFake = Crypto.decodePrivateKey(encodedPrivK1)
|
||||||
|
println("OK")
|
||||||
|
fail()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// expected
|
||||||
|
}
|
||||||
|
encodedPrivK1.set(i,b.dec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
103
core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt
Normal file
103
core/src/test/kotlin/net/corda/core/crypto/EncodingUtilsTest.kt
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import org.junit.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
class EncodingUtilsTest {
|
||||||
|
|
||||||
|
val testString = "Hello World"
|
||||||
|
val testBytes = testString.toByteArray()
|
||||||
|
val testBase58String = "JxF12TrwUP45BMd" // Base58 format for Hello World.
|
||||||
|
val testBase64String = "SGVsbG8gV29ybGQ=" // Base64 format for Hello World.
|
||||||
|
val testHexString = "48656C6C6F20576F726C64" // HEX format for Hello World.
|
||||||
|
|
||||||
|
// Encoding tests
|
||||||
|
@Test
|
||||||
|
fun `encoding Hello World`() {
|
||||||
|
assertEquals(testBase58String, testBytes.toBase58())
|
||||||
|
assertEquals(testBase64String, testBytes.toBase64())
|
||||||
|
assertEquals(testHexString, testBytes.toHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `empty encoding`() {
|
||||||
|
val emptyByteArray = ByteArray(0)
|
||||||
|
assertEquals("", emptyByteArray.toBase58())
|
||||||
|
assertEquals("", emptyByteArray.toBase64())
|
||||||
|
assertEquals("", emptyByteArray.toHex())
|
||||||
|
}
|
||||||
|
@Test
|
||||||
|
fun `encoding 7 zero bytes`() {
|
||||||
|
val sevenZeroByteArray = ByteArray(7)
|
||||||
|
assertEquals("1111111", sevenZeroByteArray.toBase58())
|
||||||
|
assertEquals("AAAAAAAAAA==", sevenZeroByteArray.toBase64())
|
||||||
|
assertEquals("00000000000000", sevenZeroByteArray.toHex())
|
||||||
|
}
|
||||||
|
|
||||||
|
//Decoding tests
|
||||||
|
@Test
|
||||||
|
fun `decoding to real String`() {
|
||||||
|
assertEquals(testString, testBase58String.base58ToRealString())
|
||||||
|
assertEquals(testString, testBase64String.base64ToRealString())
|
||||||
|
assertEquals(testString, testHexString.hexToRealString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decoding empty Strings`() {
|
||||||
|
assertEquals("", "".base58ToRealString())
|
||||||
|
assertEquals("", "".base64ToRealString())
|
||||||
|
assertEquals("", "".hexToRealString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decoding lowercase and mixed HEX`() {
|
||||||
|
val testHexStringLowercase = testHexString.toLowerCase()
|
||||||
|
assertEquals(testHexString.hexToRealString(), testHexStringLowercase.hexToRealString())
|
||||||
|
|
||||||
|
val testHexStringMixed = testHexString.replace('C', 'c')
|
||||||
|
assertEquals(testHexString.hexToRealString(), testHexStringMixed.hexToRealString())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `decoding on wrong format`() {
|
||||||
|
// the String "Hello World" is not a valid Base58 or Base64 or HEX format
|
||||||
|
try {
|
||||||
|
testString.base58ToRealString()
|
||||||
|
fail()
|
||||||
|
} catch (e: AddressFormatException) {
|
||||||
|
// expected.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
testString.base64ToRealString()
|
||||||
|
fail()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// expected.
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
testString.hexToRealString()
|
||||||
|
fail()
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
// expected.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Encoding changers tests
|
||||||
|
@Test
|
||||||
|
fun `change encoding between base58, base64, hex`() {
|
||||||
|
// base58 to base64
|
||||||
|
assertEquals(testBase64String, testBase58String.base58toBase64())
|
||||||
|
// base58 to hex
|
||||||
|
assertEquals(testHexString, testBase58String.base58toHex())
|
||||||
|
// base64 to base58
|
||||||
|
assertEquals(testBase58String, testBase64String.base64toBase58())
|
||||||
|
// base64 to hex
|
||||||
|
assertEquals(testHexString, testBase64String.base64toHex())
|
||||||
|
// hex to base58
|
||||||
|
assertEquals(testBase58String, testHexString.hexToBase58())
|
||||||
|
// hex to base64
|
||||||
|
assertEquals(testBase64String, testHexString.hexToBase64())
|
||||||
|
}
|
||||||
|
}
|
@ -12,9 +12,9 @@ class PartyTest {
|
|||||||
val differentKey = entropyToKeyPair(BigInteger.valueOf(7201702L)).public.composite
|
val differentKey = entropyToKeyPair(BigInteger.valueOf(7201702L)).public.composite
|
||||||
val anonymousParty = AnonymousParty(key)
|
val anonymousParty = AnonymousParty(key)
|
||||||
val party = Party("test key", key)
|
val party = Party("test key", key)
|
||||||
assertEquals(party, anonymousParty)
|
assertEquals<AbstractParty>(party, anonymousParty)
|
||||||
assertEquals(anonymousParty, party)
|
assertEquals<AbstractParty>(anonymousParty, party)
|
||||||
assertNotEquals(AnonymousParty(differentKey), anonymousParty)
|
assertNotEquals<AbstractParty>(AnonymousParty(differentKey), anonymousParty)
|
||||||
assertNotEquals(AnonymousParty(differentKey), party)
|
assertNotEquals<AbstractParty>(AnonymousParty(differentKey), party)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,83 @@
|
|||||||
|
package net.corda.core.crypto
|
||||||
|
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider
|
||||||
|
import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
|
||||||
|
import org.junit.Test
|
||||||
|
import java.security.Security
|
||||||
|
import java.security.SignatureException
|
||||||
|
import java.time.Instant
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
import kotlin.test.fail
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digital signature MetaData tests
|
||||||
|
*/
|
||||||
|
class TransactionSignatureTest {
|
||||||
|
|
||||||
|
init {
|
||||||
|
Security.addProvider(BouncyCastleProvider())
|
||||||
|
Security.addProvider(BouncyCastlePQCProvider())
|
||||||
|
}
|
||||||
|
|
||||||
|
val testBytes = "12345678901234567890123456789012".toByteArray()
|
||||||
|
|
||||||
|
/** valid sign and verify. */
|
||||||
|
@Test
|
||||||
|
fun `MetaData Full sign and verify`() {
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
|
||||||
|
// create a MetaData.Full object
|
||||||
|
val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair.public)
|
||||||
|
|
||||||
|
// sign the message
|
||||||
|
val transactionSignature: TransactionSignature = keyPair.private.sign(meta)
|
||||||
|
|
||||||
|
// check auto-verification
|
||||||
|
assertTrue(transactionSignature.verify())
|
||||||
|
|
||||||
|
// check manual verification
|
||||||
|
assertTrue(keyPair.public.verify(transactionSignature))
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Signing should fail, as I sign with a secpK1 key, but set schemeCodeName is set to secpR1. */
|
||||||
|
@Test(expected = IllegalArgumentException::class)
|
||||||
|
fun `MetaData Full failure wrong scheme`() {
|
||||||
|
val keyPair = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val meta = MetaData("ECDSA_SECP256R1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair.public)
|
||||||
|
keyPair.private.sign(meta)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verification should fail; corrupted metadata - public key has changed. */
|
||||||
|
@Test(expected = SignatureException::class)
|
||||||
|
fun `MetaData Full failure public key has changed`() {
|
||||||
|
val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val keyPair2 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair2.public)
|
||||||
|
val transactionSignature = keyPair1.private.sign(meta)
|
||||||
|
transactionSignature.verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verification should fail; corrupted metadata - clearData has changed. */
|
||||||
|
@Test(expected = SignatureException::class)
|
||||||
|
fun `MetaData Full failure clearData has changed`() {
|
||||||
|
val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair1.public)
|
||||||
|
val transactionSignature = keyPair1.private.sign(meta)
|
||||||
|
|
||||||
|
val meta2 = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes.plus(testBytes), keyPair1.public)
|
||||||
|
val transactionSignature2 = TransactionSignature(transactionSignature.signatureData, meta2)
|
||||||
|
keyPair1.public.verify(transactionSignature2)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Verification should fail; corrupted metadata - schemeCodeName has changed from K1 to R1. */
|
||||||
|
@Test(expected = SignatureException::class)
|
||||||
|
fun `MetaData Wrong schemeCodeName has changed`() {
|
||||||
|
val keyPair1 = Crypto.generateKeyPair("ECDSA_SECP256K1_SHA256")
|
||||||
|
val meta = MetaData("ECDSA_SECP256K1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes, keyPair1.public)
|
||||||
|
val transactionSignature = keyPair1.private.sign(meta)
|
||||||
|
|
||||||
|
val meta2 = MetaData("ECDSA_SECP256R1_SHA256", "M9", SignatureType.FULL, Instant.now(), null, null, testBytes.plus(testBytes), keyPair1.public)
|
||||||
|
val transactionSignature2 = TransactionSignature(transactionSignature.signatureData, meta2)
|
||||||
|
keyPair1.public.verify(transactionSignature2)
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package net.corda.core.flows
|
package net.corda.core.flows
|
||||||
|
|
||||||
import com.esotericsoftware.kryo.io.Input
|
|
||||||
import com.pholser.junit.quickcheck.From
|
import com.pholser.junit.quickcheck.From
|
||||||
import com.pholser.junit.quickcheck.Property
|
import com.pholser.junit.quickcheck.Property
|
||||||
import com.pholser.junit.quickcheck.generator.GenerationStatus
|
import com.pholser.junit.quickcheck.generator.GenerationStatus
|
||||||
@ -8,7 +7,7 @@ import com.pholser.junit.quickcheck.generator.Generator
|
|||||||
import com.pholser.junit.quickcheck.random.SourceOfRandomness
|
import com.pholser.junit.quickcheck.random.SourceOfRandomness
|
||||||
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
||||||
import net.corda.contracts.testing.SignedTransactionGenerator
|
import net.corda.contracts.testing.SignedTransactionGenerator
|
||||||
import net.corda.core.serialization.createKryo
|
import net.corda.core.serialization.deserialize
|
||||||
import net.corda.core.serialization.serialize
|
import net.corda.core.serialization.serialize
|
||||||
import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest
|
import net.corda.flows.BroadcastTransactionFlow.NotifyTxRequest
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@ -25,9 +24,8 @@ class BroadcastTransactionFlowTest {
|
|||||||
|
|
||||||
@Property
|
@Property
|
||||||
fun serialiseDeserialiseOfNotifyMessageWorks(@From(NotifyTxRequestMessageGenerator::class) message: NotifyTxRequest) {
|
fun serialiseDeserialiseOfNotifyMessageWorks(@From(NotifyTxRequestMessageGenerator::class) message: NotifyTxRequest) {
|
||||||
val kryo = createKryo()
|
|
||||||
val serialized = message.serialize().bytes
|
val serialized = message.serialize().bytes
|
||||||
val deserialized = kryo.readClassAndObject(Input(serialized))
|
val deserialized = serialized.deserialize<NotifyTxRequest>()
|
||||||
assertEquals(deserialized, message)
|
assertEquals(deserialized, message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,17 @@ import net.corda.core.crypto.CompositeKey
|
|||||||
import net.corda.core.crypto.Party
|
import net.corda.core.crypto.Party
|
||||||
import net.corda.core.crypto.SecureHash
|
import net.corda.core.crypto.SecureHash
|
||||||
import net.corda.core.getOrThrow
|
import net.corda.core.getOrThrow
|
||||||
|
import net.corda.core.messaging.startFlow
|
||||||
|
import net.corda.core.node.services.unconsumedStates
|
||||||
import net.corda.core.serialization.OpaqueBytes
|
import net.corda.core.serialization.OpaqueBytes
|
||||||
import net.corda.core.utilities.Emoji
|
import net.corda.core.utilities.Emoji
|
||||||
import net.corda.flows.CashIssueFlow
|
import net.corda.flows.CashIssueFlow
|
||||||
import net.corda.flows.ContractUpgradeFlow
|
import net.corda.flows.ContractUpgradeFlow
|
||||||
import net.corda.flows.FinalityFlow
|
import net.corda.flows.FinalityFlow
|
||||||
|
import net.corda.node.internal.CordaRPCOpsImpl
|
||||||
|
import net.corda.node.services.User
|
||||||
|
import net.corda.node.services.messaging.CURRENT_RPC_USER
|
||||||
|
import net.corda.node.services.startFlowPermission
|
||||||
import net.corda.node.utilities.databaseTransaction
|
import net.corda.node.utilities.databaseTransaction
|
||||||
import net.corda.testing.node.MockNetwork
|
import net.corda.testing.node.MockNetwork
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@ -60,19 +66,82 @@ class ContractUpgradeFlowTest {
|
|||||||
requireNotNull(btx)
|
requireNotNull(btx)
|
||||||
|
|
||||||
// The request is expected to be rejected because party B haven't authorise the upgrade yet.
|
// The request is expected to be rejected because party B haven't authorise the upgrade yet.
|
||||||
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Instigator<DummyContract.State, DummyContractV2.State>(atx!!.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture
|
val rejectedFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx!!.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||||
|
|
||||||
// Party B authorise the contract state upgrade.
|
// Party B authorise the contract state upgrade.
|
||||||
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DUMMY_V2_PROGRAM_ID.javaClass)
|
b.services.vaultService.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||||
|
|
||||||
// Party A initiates contract upgrade flow, expected to succeed this time.
|
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||||
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator<DummyContract.State, DummyContractV2.State>(atx.tx.outRef(0), DUMMY_V2_PROGRAM_ID.javaClass)).resultFuture
|
val resultFuture = a.services.startFlow(ContractUpgradeFlow.Instigator(atx.tx.outRef(0), DummyContractV2::class.java)).resultFuture
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
|
|
||||||
val result = resultFuture.get()
|
val result = resultFuture.get()
|
||||||
|
|
||||||
|
fun check(node: MockNetwork.MockNode) {
|
||||||
|
val nodeStx = databaseTransaction(node.database) {
|
||||||
|
node.services.storageService.validatedTransactions.getTransaction(result.ref.txhash)
|
||||||
|
}
|
||||||
|
requireNotNull(nodeStx)
|
||||||
|
|
||||||
|
// Verify inputs.
|
||||||
|
val input = databaseTransaction(node.database) {
|
||||||
|
node.services.storageService.validatedTransactions.getTransaction(nodeStx!!.tx.inputs.single().txhash)
|
||||||
|
}
|
||||||
|
requireNotNull(input)
|
||||||
|
assertTrue(input!!.tx.outputs.single().data is DummyContract.State)
|
||||||
|
|
||||||
|
// Verify outputs.
|
||||||
|
assertTrue(nodeStx!!.tx.outputs.single().data is DummyContractV2.State)
|
||||||
|
}
|
||||||
|
check(a)
|
||||||
|
check(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `2 parties contract upgrade using RPC`() {
|
||||||
|
// Create dummy contract.
|
||||||
|
val twoPartyDummyContract = DummyContract.generateInitial(0, notary, a.info.legalIdentity.ref(1), b.info.legalIdentity.ref(1))
|
||||||
|
val stx = twoPartyDummyContract.signWith(a.services.legalIdentityKey)
|
||||||
|
.signWith(b.services.legalIdentityKey)
|
||||||
|
.toSignedTransaction()
|
||||||
|
|
||||||
|
a.services.startFlow(FinalityFlow(stx, setOf(a.info.legalIdentity, b.info.legalIdentity)))
|
||||||
|
mockNet.runNetwork()
|
||||||
|
|
||||||
|
val atx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(stx.id) }
|
||||||
|
val btx = databaseTransaction(b.database) { b.services.storageService.validatedTransactions.getTransaction(stx.id) }
|
||||||
|
requireNotNull(atx)
|
||||||
|
requireNotNull(btx)
|
||||||
|
|
||||||
|
// The request is expected to be rejected because party B haven't authorise the upgrade yet.
|
||||||
|
|
||||||
|
val rpcA = CordaRPCOpsImpl(a.services, a.smm, a.database)
|
||||||
|
val rpcB = CordaRPCOpsImpl(b.services, b.smm, b.database)
|
||||||
|
|
||||||
|
CURRENT_RPC_USER.set(User("user", "pwd", permissions = setOf(
|
||||||
|
startFlowPermission<ContractUpgradeFlow.Instigator<*, *>>()
|
||||||
|
)))
|
||||||
|
|
||||||
|
val rejectedFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
|
||||||
|
atx!!.tx.outRef<DummyContract.State>(0),
|
||||||
|
DummyContractV2::class.java).returnValue
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||||
|
|
||||||
|
// Party B authorise the contract state upgrade.
|
||||||
|
rpcB.authoriseContractUpgrade(btx!!.tx.outRef<ContractState>(0), DummyContractV2::class.java)
|
||||||
|
|
||||||
|
// Party A initiates contract upgrade flow, expected to succeed this time.
|
||||||
|
val resultFuture = rpcA.startFlow({ stateAndRef, upgrade -> ContractUpgradeFlow.Instigator(stateAndRef, upgrade) },
|
||||||
|
atx.tx.outRef<DummyContract.State>(0),
|
||||||
|
DummyContractV2::class.java).returnValue
|
||||||
|
|
||||||
|
mockNet.runNetwork()
|
||||||
|
val result = resultFuture.get()
|
||||||
|
// Check results.
|
||||||
listOf(a, b).forEach {
|
listOf(a, b).forEach {
|
||||||
val stx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
val stx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
||||||
requireNotNull(stx)
|
requireNotNull(stx)
|
||||||
@ -94,17 +163,17 @@ class ContractUpgradeFlowTest {
|
|||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
|
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
|
||||||
// Starts contract upgrade flow.
|
// Starts contract upgrade flow.
|
||||||
a.services.startFlow(ContractUpgradeFlow.Instigator<Cash.State, CashV2.State>(stateAndRef, CashV2().javaClass))
|
a.services.startFlow(ContractUpgradeFlow.Instigator(stateAndRef, CashV2::class.java))
|
||||||
mockNet.runNetwork()
|
mockNet.runNetwork()
|
||||||
// Get contract state form the vault.
|
// Get contract state form the vault.
|
||||||
val state = databaseTransaction(a.database) { a.vault.currentVault.states }
|
val state = databaseTransaction(a.database) { a.vault.unconsumedStates<ContractState>() }
|
||||||
assertTrue(state.single().state.data is CashV2.State, "Contract state is upgraded to the new version.")
|
assertTrue(state.single().state.data is CashV2.State, "Contract state is upgraded to the new version.")
|
||||||
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (state.first().state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
|
assertEquals(Amount(1000000, USD).`issued by`(a.info.legalIdentity.ref(1)), (state.first().state.data as CashV2.State).amount, "Upgraded cash contain the correct amount.")
|
||||||
assertEquals(listOf(a.info.legalIdentity.owningKey), (state.first().state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
|
assertEquals(listOf(a.info.legalIdentity.owningKey), (state.first().state.data as CashV2.State).owners, "Upgraded cash belongs to the right owner.")
|
||||||
}
|
}
|
||||||
|
|
||||||
class CashV2 : UpgradedContract<Cash.State, CashV2.State> {
|
class CashV2 : UpgradedContract<Cash.State, CashV2.State> {
|
||||||
override val legacyContract = Cash()
|
override val legacyContract = Cash::class.java
|
||||||
|
|
||||||
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<CompositeKey>) : FungibleAsset<Currency> {
|
data class State(override val amount: Amount<Issued<Currency>>, val owners: List<CompositeKey>) : FungibleAsset<Currency> {
|
||||||
override val owner: CompositeKey = owners.first()
|
override val owner: CompositeKey = owners.first()
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user