mirror of
https://github.com/corda/corda.git
synced 2024-12-28 00:38:55 +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/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>
|
4
LICENSE
4
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright 2016, R3 Limited.
|
||||
Copyright 2016 - 2017, R3 Limited.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -10,4 +10,4 @@
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
|
@ -55,6 +55,9 @@ To look at the Corda source and run some sample applications:
|
||||
* [Documentation](https://docs.corda.net)
|
||||
* [Slack channel] (https://slack.corda.net/)
|
||||
* [Forum](https://discourse.corda.net)
|
||||
* [Meetups](https://www.meetup.com/pro/corda/)
|
||||
* [Training Course](https://www.corda.net/corda-training/)
|
||||
|
||||
|
||||
## Development State
|
||||
|
||||
@ -77,7 +80,7 @@ Please read [here](./CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
[Apache 2.0](./LICENCE)
|
||||
[Apache 2.0](./LICENSE)
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
|
28
build.gradle
28
build.gradle
@ -4,16 +4,16 @@ buildscript {
|
||||
file("publish.properties").withInputStream { props.load(it) }
|
||||
|
||||
// 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")
|
||||
|
||||
// Dependency versions. Can run 'gradle dependencyUpdates' to find new versions of things.
|
||||
//
|
||||
// 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.asm_version = '0.5.3'
|
||||
ext.artemis_version = '1.5.1'
|
||||
ext.artemis_version = '1.5.3'
|
||||
ext.jackson_version = '2.8.5'
|
||||
ext.jetty_version = '9.3.9.v20160517'
|
||||
ext.jersey_version = '2.25'
|
||||
@ -24,12 +24,15 @@ buildscript {
|
||||
ext.guava_version = '19.0'
|
||||
ext.quickcheck_version = '0.7'
|
||||
ext.okhttp_version = '3.5.0'
|
||||
ext.netty_version = '4.1.5.Final'
|
||||
ext.typesafe_config_version = '1.3.1'
|
||||
ext.junit_version = '4.12'
|
||||
ext.jopt_simple_version = '5.0.2'
|
||||
ext.jansi_version = '1.14'
|
||||
ext.hibernate_version = '5.2.6.Final'
|
||||
ext.h2_version = '1.4.193'
|
||||
ext.rxjava_version = '1.2.4'
|
||||
ext.requery_version = '1.1.1'
|
||||
ext.dokka_version = '0.9.13'
|
||||
|
||||
repositories {
|
||||
@ -44,7 +47,9 @@ buildscript {
|
||||
classpath "net.corda.plugins:quasar-utils:$gradle_plugins_version"
|
||||
classpath "net.corda.plugins:cordformation:$gradle_plugins_version"
|
||||
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.ajoberstar:grgit:1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,6 +59,10 @@ plugins {
|
||||
id "us.kirchmeier.capsule" version "1.0.2"
|
||||
}
|
||||
|
||||
ext {
|
||||
corda_revision = org.ajoberstar.grgit.Grgit.open(file('.')).head().id
|
||||
}
|
||||
|
||||
apply plugin: 'kotlin'
|
||||
apply plugin: 'project-report'
|
||||
apply plugin: 'com.github.ben-manes.versions'
|
||||
@ -84,6 +93,10 @@ allprojects {
|
||||
|
||||
group 'net.corda'
|
||||
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
|
||||
@ -107,6 +120,7 @@ dependencies {
|
||||
compile project(':node')
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
runtime project(path: ":node:capsule", configuration: 'runtimeArtifacts')
|
||||
runtime project(path: ":node:webserver", configuration: 'runtimeArtifacts')
|
||||
}
|
||||
|
||||
task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
|
||||
@ -172,7 +186,7 @@ bintrayConfig {
|
||||
projectUrl = 'https://github.com/corda/corda'
|
||||
gpgSign = true
|
||||
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 {
|
||||
name = 'Apache-2.0'
|
||||
url = 'https://www.apache.org/licenses/LICENSE-2.0'
|
||||
@ -191,7 +205,7 @@ dokka {
|
||||
moduleName = 'corda'
|
||||
outputDirectory = 'docs/build/html/api/kotlin'
|
||||
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) {
|
||||
@ -199,7 +213,7 @@ task dokkaJavadoc(type: org.jetbrains.dokka.gradle.DokkaTask) {
|
||||
outputFormat = "javadoc"
|
||||
outputDirectory = 'docs/build/html/api/javadoc'
|
||||
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.JsonParseException
|
||||
@ -13,10 +13,10 @@ import net.corda.core.contracts.BusinessCalendar
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.i2p.crypto.eddsa.EdDSAPublicKey
|
||||
import net.corda.core.node.services.IdentityService
|
||||
import java.math.BigDecimal
|
||||
import java.time.LocalDate
|
||||
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
|
||||
* 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 {
|
||||
fun partyFromName(partyName: String): Party?
|
||||
fun partyFromKey(owningKey: CompositeKey): Party?
|
||||
}
|
||||
|
||||
class RpcObjectMapper(val rpc: CordaRPCOps) : PartyObjectMapper, ObjectMapper() {
|
||||
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(){
|
||||
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() {
|
||||
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 {
|
||||
@ -53,6 +59,8 @@ object JsonSupport {
|
||||
|
||||
val cordaModule: Module by lazy {
|
||||
SimpleModule("core").apply {
|
||||
addSerializer(AnonymousParty::class.java, AnonymousPartySerializer)
|
||||
addDeserializer(AnonymousParty::class.java, AnonymousPartyDeserializer)
|
||||
addSerializer(Party::class.java, PartySerializer)
|
||||
addDeserializer(Party::class.java, PartyDeserializer)
|
||||
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))
|
||||
|
||||
/* 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())
|
||||
|
||||
/* For testing with an in memory identity service */
|
||||
/** For testing with an in memory identity service */
|
||||
@JvmStatic
|
||||
fun createInMemoryMapper(identityService: IdentityService) = configureMapper(IdentityObjectMapper(identityService))
|
||||
|
||||
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>() {
|
||||
override fun serialize(obj: Party, generator: JsonGenerator, provider: SerializerProvider) {
|
||||
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.Property
|
||||
import com.pholser.junit.quickcheck.runner.JUnitQuickcheck
|
||||
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 org.junit.runner.RunWith
|
||||
import java.security.PublicKey
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@RunWith(JUnitQuickcheck::class)
|
||||
class JsonSupportTest {
|
||||
|
||||
class JacksonSupportTest {
|
||||
companion object {
|
||||
val mapper = JsonSupport.createNonRpcMapper()
|
||||
val mapper = JacksonSupport.createNonRpcMapper()
|
||||
}
|
||||
|
||||
@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.pickN(number: Int, list: List<A>) = Generator<List<A>> {
|
||||
val mask = BitSet(list.size)
|
||||
for (i in 0..Math.min(list.size, number) - 1) {
|
||||
mask[i] = 1
|
||||
val size = Math.min(list.size, number)
|
||||
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) {
|
||||
val byte = mask[i]
|
||||
val swapIndex = i + it.nextInt(mask.size() - i)
|
||||
for (i in 0..list.size - 1) {
|
||||
val bit = mask[i]
|
||||
val swapIndex = i + it.nextInt(size - i)
|
||||
mask[i] = mask[swapIndex]
|
||||
mask[swapIndex] = byte
|
||||
mask[swapIndex] = bit
|
||||
}
|
||||
val resultList = ArrayList<A>()
|
||||
list.forEachIndexed { index, a ->
|
||||
|
@ -40,6 +40,7 @@ class NetworkIdentityModel {
|
||||
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 }) {
|
||||
it.legalIdentity.owningKey == compositeKey
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31337"
|
||||
webAddress : "localhost:31339"
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:12345"
|
||||
legalName : "Network Map Service"
|
||||
|
@ -4,7 +4,7 @@ keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:31338"
|
||||
webAddress : "localhost:31340"
|
||||
extraAdvertisedServiceIds: "corda.interest_rates"
|
||||
extraAdvertisedServiceIds : [ "corda.interest_rates" ]
|
||||
networkMapService : {
|
||||
address : "localhost:12345"
|
||||
legalName : "Network Map Service"
|
||||
|
@ -6,17 +6,14 @@
|
||||
<Property name="log-name">node-${hostName}</Property>
|
||||
<Property name="archive">${sys:log-path}/archive</Property>
|
||||
<Property name="consoleLogLevel">error</Property>
|
||||
<Property name="defaultLogLevel">info</Property>
|
||||
</Properties>
|
||||
|
||||
<ThresholdFilter level="trace"/>
|
||||
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<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>
|
||||
<PatternLayout pattern="%highlight{%level{length=1} %d{HH:mm:ss} [%t] %c{2}.%M - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" />
|
||||
</Console>
|
||||
|
||||
<!-- 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"
|
||||
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>
|
||||
<TimeBasedTriggeringPolicy/>
|
||||
@ -47,13 +44,9 @@
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<Root level="${sys:defaultLogLevel}">
|
||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-Appender" level="info"/>
|
||||
<AppenderRef ref="RollingFile-Appender" />
|
||||
</Root>
|
||||
<Logger name="net.corda" level="info" additivity="false">
|
||||
<AppenderRef ref="Console-Appender" level="${sys:consoleLogLevel}"/>
|
||||
<AppenderRef ref="RollingFile-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
@ -4,5 +4,5 @@ keyStorePassword : "cordacadevpass"
|
||||
trustStorePassword : "trustpass"
|
||||
artemisAddress : "localhost:12345"
|
||||
webAddress : "localhost:12346"
|
||||
extraAdvertisedServiceIds: "corda.notary.validating"
|
||||
extraAdvertisedServiceIds : [ "corda.notary.validating" ]
|
||||
useHTTPS : false
|
@ -1,19 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="info">
|
||||
<Properties>
|
||||
<Property name="defaultLogLevel">info</Property>
|
||||
</Properties>
|
||||
<Appenders>
|
||||
<Console name="Console-Appender" target="SYSTEM_OUT">
|
||||
<PatternLayout>
|
||||
<pattern>
|
||||
[%-5level] %d{HH:mm:ss.SSS} [%t] %c{1}.%M - %msg%n
|
||||
</pattern>>
|
||||
</PatternLayout>
|
||||
<PatternLayout pattern="[%-5level] %d{HH:mm:ss.SSS} [%t] %c{2}.%M - %msg%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Root level="info">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Root>
|
||||
<Logger name="net.corda" level="info" additivity="false">
|
||||
<Logger name="net.corda" level="${sys:defaultLogLevel}" additivity="false">
|
||||
<AppenderRef ref="Console-Appender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
|
@ -31,6 +31,7 @@ sourceSets {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
testCompile "junit:junit:$junit_version"
|
||||
testCompile "commons-fileupload:commons-fileupload:1.3.2"
|
||||
|
||||
@ -56,6 +57,9 @@ dependencies {
|
||||
// AssertJ: for fluent assertions for testing
|
||||
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-generators:$quickcheck_version"
|
||||
|
||||
@ -63,7 +67,7 @@ dependencies {
|
||||
compile "com.google.guava:guava:$guava_version"
|
||||
|
||||
// RxJava: observable streams of events.
|
||||
compile "io.reactivex:rxjava:1.2.4"
|
||||
compile "io.reactivex:rxjava:$rxjava_version"
|
||||
|
||||
// Kryo: object graph serialization.
|
||||
compile "com.esotericsoftware:kryo:4.0.0"
|
||||
@ -88,4 +92,7 @@ dependencies {
|
||||
|
||||
// RS API: Response type and codes for ApiUtils.
|
||||
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 kotlinx.support.jdk7.use
|
||||
import net.corda.core.crypto.newSecureRandom
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
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)
|
||||
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 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()
|
||||
block()
|
||||
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
|
||||
@ -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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
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.
|
||||
*/
|
||||
fun extractZipFile(zipFile: Path, toDirectory: Path) {
|
||||
val normalisedDirectory = toDirectory.normalize().createDirectories()
|
||||
fun extractZipFile(zipFile: Path, toDirectory: Path) = extractZipFile(Files.newInputStream(zipFile), toDirectory)
|
||||
|
||||
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) {
|
||||
val e = zip.nextEntry ?: break
|
||||
val e = it.nextEntry ?: break
|
||||
val outPath = (normalisedDirectory / e.name).normalize()
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
|
||||
/** Representation of an operation that may have thrown an error. */
|
||||
@CordaSerializable
|
||||
data class ErrorOr<out A> private constructor(val value: A?, val error: Throwable?) {
|
||||
// The ErrorOr holds a value iff error == null
|
||||
constructor(value: A) : this(value, null)
|
||||
@ -391,15 +398,24 @@ private class ObservableToFuture<T>(observable: Observable<T>) : AbstractFuture<
|
||||
override fun onNext(value: T) {
|
||||
set(value)
|
||||
}
|
||||
|
||||
override fun onError(e: Throwable) {
|
||||
setException(e)
|
||||
}
|
||||
|
||||
override fun cancel(mayInterruptIfRunning: Boolean): Boolean {
|
||||
subscription.unsubscribe()
|
||||
return super.cancel(mayInterruptIfRunning)
|
||||
}
|
||||
|
||||
override fun onCompleted() {}
|
||||
}
|
||||
|
||||
/** Return the sum of an Iterable of [BigDecimal]s. */
|
||||
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.
|
||||
*/
|
||||
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 {
|
||||
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.JsonSerialize
|
||||
import com.google.common.annotations.VisibleForTesting
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.math.BigDecimal
|
||||
import java.math.BigInteger
|
||||
import java.time.DayOfWeek
|
||||
@ -34,6 +35,7 @@ import java.util.*
|
||||
*
|
||||
* @param T the type of the token, for example [Currency].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Amount<T>(val quantity: Long, val token: T) : Comparable<Amount<T>> {
|
||||
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) */
|
||||
@CordaSerializable
|
||||
data class FixOf(val name: String, val forDay: LocalDate, val ofTenor: Tenor)
|
||||
|
||||
/** A [Fix] represents a named interest rate, on a given day, for a given duration. It can be embedded in a tx. */
|
||||
data class Fix(val of: FixOf, val value: BigDecimal) : CommandData
|
||||
|
||||
/** Represents a textual expression of e.g. a formula */
|
||||
@CordaSerializable
|
||||
@JsonDeserialize(using = ExpressionDeserializer::class)
|
||||
@JsonSerialize(using = ExpressionSerializer::class)
|
||||
data class Expression(val expr: String)
|
||||
@ -131,6 +135,7 @@ object ExpressionDeserializer : JsonDeserializer<Expression>() {
|
||||
}
|
||||
|
||||
/** Placeholder class for the Tenor datatype - which is a standardised duration of time until maturity */
|
||||
@CordaSerializable
|
||||
data class Tenor(val name: String) {
|
||||
private val amount: Int
|
||||
private val unit: TimeUnit
|
||||
@ -166,6 +171,7 @@ data class Tenor(val name: String) {
|
||||
|
||||
override fun toString(): String = name
|
||||
|
||||
@CordaSerializable
|
||||
enum class TimeUnit(val code: String) {
|
||||
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.
|
||||
* We don't actually do anything with this yet though, so it's ignored for now.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class AccrualAdjustment {
|
||||
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
|
||||
* finding a business day.
|
||||
*/
|
||||
@CordaSerializable
|
||||
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.
|
||||
* There are some additional rules which are explained in the individual cases below.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class DateRollConvention {
|
||||
// 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
|
||||
* in the toString lest some people get confused.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class DayCountBasisDay {
|
||||
// We have to prefix 30 etc with a letter due to enum naming constraints.
|
||||
D30,
|
||||
@ -246,6 +256,7 @@ enum class DayCountBasisDay {
|
||||
}
|
||||
|
||||
/** This forms the year part of the "Day Count Basis" used for interest calculation. */
|
||||
@CordaSerializable
|
||||
enum class DayCountBasisYear {
|
||||
// Ditto above comment for years.
|
||||
Y360,
|
||||
@ -257,6 +268,7 @@ enum class DayCountBasisYear {
|
||||
}
|
||||
|
||||
/** Whether the payment should be made before the due date, or after it. */
|
||||
@CordaSerializable
|
||||
enum class PaymentRule {
|
||||
InAdvance, InArrears,
|
||||
}
|
||||
@ -266,6 +278,7 @@ enum class PaymentRule {
|
||||
* that would divide into (eg annually = 1, semiannual = 2, monthly = 12 etc).
|
||||
*/
|
||||
@Suppress("unused") // TODO: Revisit post-Vega and see if annualCompoundCount is still needed.
|
||||
@CordaSerializable
|
||||
enum class Frequency(val annualCompoundCount: Int) {
|
||||
Annual(1) {
|
||||
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
|
||||
* no staff are around to handle problems.
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class BusinessCalendar private constructor(val holidayDates: List<LocalDate>) {
|
||||
@CordaSerializable
|
||||
class UnknownCalendar(name: String) : Exception("$name not found")
|
||||
|
||||
companion object {
|
||||
@ -434,6 +449,7 @@ fun calculateDaysBetween(startDate: LocalDate,
|
||||
* Enum for the types of netting that can be applied to state objects. Exact behaviour
|
||||
* for each type of netting is left to the contract to determine.
|
||||
*/
|
||||
@CordaSerializable
|
||||
enum class NetType {
|
||||
/**
|
||||
* Close-out netting applies where one party is bankrupt or otherwise defaults (exact terms are contract specific),
|
||||
@ -461,6 +477,7 @@ enum class NetType {
|
||||
* @param defaultFractionDigits the number of digits normally after the decimal point when referring to quantities of
|
||||
* this commodity.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Commodity(val commodityCode: String,
|
||||
val displayName: String,
|
||||
val defaultFractionDigits: Int = 0) {
|
||||
@ -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.
|
||||
* Subsequent copies and evolutions of a state should just copy the [externalId] and [id] fields unmodified.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class UniqueIdentifier(val externalId: String? = null, val id: UUID = UUID.randomUUID()) : Comparable<UniqueIdentifier> {
|
||||
override fun toString(): String = if (externalId != null) "${externalId}_$id" else id.toString()
|
||||
|
||||
|
@ -8,6 +8,7 @@ import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.FlowLogicRef
|
||||
import net.corda.core.flows.FlowLogicRefFactory
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.corda.core.serialization.serialize
|
||||
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
|
||||
* are all free.
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface ContractState {
|
||||
/**
|
||||
* 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.
|
||||
* 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(
|
||||
/** The custom contract state */
|
||||
val data: T,
|
||||
@ -169,6 +172,7 @@ interface IssuanceDefinition
|
||||
*
|
||||
* @param P the type of product underlying the definition, for example [Currency].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Issued<out P>(val issuer: PartyAndReference, val product: P) {
|
||||
override fun toString() = "$product issued by $issuer"
|
||||
}
|
||||
@ -239,6 +243,7 @@ interface LinearState : ContractState {
|
||||
/**
|
||||
* Standard clause to verify the LinearState safety properties.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ClauseVerifier<S : LinearState, C : CommandData>() : Clause<S, C, Unit>() {
|
||||
override fun verify(tx: TransactionForContract,
|
||||
inputs: List<S>,
|
||||
@ -290,7 +295,7 @@ interface DealState : LinearState {
|
||||
* separate process exchange certificates to ascertain identities. Thus decoupling identities from
|
||||
* [ContractState]s.
|
||||
* */
|
||||
val parties: List<Party>
|
||||
val parties: List<AnonymousParty>
|
||||
|
||||
/**
|
||||
* 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
|
||||
* transaction defined the state and where in that transaction it was.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class StateRef(val txhash: SecureHash, val index: Int) {
|
||||
override fun toString() = "$txhash($index)"
|
||||
}
|
||||
|
||||
/** 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)
|
||||
|
||||
/** 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
|
||||
* ledger. The reference is intended to be encrypted so it's meaningless to anyone other than the party.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class PartyAndReference(val party: AnonymousParty, val reference: OpaqueBytes) {
|
||||
constructor(party: Party, reference: OpaqueBytes) : this(party.toAnonymous(), reference)
|
||||
override fun toString() = "${party}$reference"
|
||||
}
|
||||
|
||||
/** Marker interface for classes that represent commands */
|
||||
@CordaSerializable
|
||||
interface CommandData
|
||||
|
||||
/** 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 */
|
||||
@CordaSerializable
|
||||
data class Command(val value: CommandData, val signers: List<CompositeKey>) {
|
||||
init {
|
||||
require(signers.isNotEmpty())
|
||||
@ -398,9 +409,10 @@ interface NetCommand : CommandData {
|
||||
}
|
||||
|
||||
/** 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. */
|
||||
@CordaSerializable
|
||||
data class AuthenticatedObject<out T : Any>(
|
||||
val signers: List<CompositeKey>,
|
||||
/** 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
|
||||
* between (after, before).
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class Timestamp(val after: Instant?, val before: Instant?) {
|
||||
init {
|
||||
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
|
||||
* 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.
|
||||
*
|
||||
* TODO: Contract serialization is likely to change, so the annotation is likely temporary.
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface Contract {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -2,10 +2,12 @@ package net.corda.core.contracts
|
||||
|
||||
import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
|
||||
/** Defines transaction build & validation logic for a specific transaction type */
|
||||
@CordaSerializable
|
||||
sealed class TransactionType {
|
||||
override fun equals(other: Any?) = other?.javaClass == javaClass
|
||||
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.SecureHash
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import java.util.*
|
||||
|
||||
@ -94,6 +95,7 @@ class AttachmentResolutionException(val hash : SecureHash) : FlowException() {
|
||||
override fun toString(): String = "Attachment resolution failure for $hash"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class TransactionConflictException(val conflictRef: StateRef, val tx1: LedgerTransaction, val tx2: LedgerTransaction) : Exception()
|
||||
|
||||
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) {
|
||||
override val message: String get() = "Missing required encumbrance $missing in $inOut"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
enum class Direction {
|
||||
INPUT,
|
||||
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
|
||||
* 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 */
|
||||
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]
|
||||
// can put in the key and actual name
|
||||
override fun toString() = "${owningKey.toBase58String()} <Anonymous>"
|
||||
|
||||
fun ref(bytes: OpaqueBytes) = PartyAndReference(this, bytes)
|
||||
fun ref(vararg bytes: Byte) = ref(OpaqueBytes.of(*bytes))
|
||||
override fun nameOrNull(): String? = null
|
||||
|
||||
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.Node
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
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
|
||||
* create multi-level requirements such as *"either the CEO or 3 of 5 of his assistants need to sign"*.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class CompositeKey {
|
||||
/** Checks whether [keys] match a sufficient amount of leaf nodes */
|
||||
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
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import net.i2p.crypto.eddsa.EdDSAEngine
|
||||
import net.i2p.crypto.eddsa.EdDSAPrivateKey
|
||||
@ -21,11 +22,8 @@ fun newSecureRandom(): SecureRandom {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a digital signature. The covering field is a generic tag usable by whatever is interpreting the
|
||||
* signature. It isn't used currently, but experience from Bitcoin suggests such a feature is useful, especially when
|
||||
* building partially signed transactions.
|
||||
*/
|
||||
/** A wrapper around a digital signature. */
|
||||
@CordaSerializable
|
||||
open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
|
||||
/** A digital signature that identifies who the public key is owned by. */
|
||||
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)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "NULL"
|
||||
override fun getEncoded() = byteArrayOf(0)
|
||||
@ -48,6 +47,7 @@ object NullPublicKey : PublicKey, Comparable<PublicKey> {
|
||||
val NullCompositeKey = NullPublicKey.composite
|
||||
|
||||
// TODO: Clean up this duplication between Null and Dummy public key
|
||||
@CordaSerializable
|
||||
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
|
||||
override fun getAlgorithm() = "DUMMY"
|
||||
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. */
|
||||
@CordaSerializable
|
||||
object NullSignature : DigitalSignature.WithKey(NullPublicKey, ByteArray(32))
|
||||
|
||||
/** 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
|
||||
|
||||
import net.corda.core.crypto.MerkleTree
|
||||
import net.corda.core.crypto.SecureHash.Companion.zeroHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
||||
|
||||
|
||||
@CordaSerializable
|
||||
class MerkleTreeException(val reason: String) : Exception() {
|
||||
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).
|
||||
* If both equalities hold, we can assume that l3 and l5 belong to the transaction with root h15.
|
||||
*/
|
||||
|
||||
@CordaSerializable
|
||||
class PartialMerkleTree(val root: PartialTree) {
|
||||
/**
|
||||
* 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
|
||||
* it's easier to extract hashes used as a base for this tree.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class PartialTree {
|
||||
class IncludedLeaf(val hash: SecureHash) : PartialTree()
|
||||
class Leaf(val hash: SecureHash) : PartialTree()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.crypto
|
||||
|
||||
import net.corda.core.contracts.PartyAndReference
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import java.security.PublicKey
|
||||
|
||||
/**
|
||||
@ -9,7 +11,7 @@ import java.security.PublicKey
|
||||
* cryptographic public key primitives into a tree structure.
|
||||
*
|
||||
* For example: Alice has two key pairs (pub1/priv1 and pub2/priv2), and wants to be able to sign transactions with either of them.
|
||||
* Her advertised [Party] then has a legal [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,
|
||||
* 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
|
||||
*/
|
||||
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 */
|
||||
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
|
||||
|
||||
import com.google.common.io.BaseEncoding
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.OpaqueBytes
|
||||
import java.security.MessageDigest
|
||||
|
||||
@ -8,6 +9,7 @@ import java.security.MessageDigest
|
||||
* Container for a cryptographically secure hash value.
|
||||
* Provides utilities for generating a cryptographic hash using different algorithms (currently only SHA-256 supported).
|
||||
*/
|
||||
@CordaSerializable
|
||||
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) */
|
||||
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
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import net.corda.core.serialization.deserialize
|
||||
import java.security.SignatureException
|
||||
@ -11,6 +12,7 @@ import java.security.SignatureException
|
||||
* @param raw the raw serialized data.
|
||||
* @param sig the (unverified) signature for the data.
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class SignedData<T : Any>(val raw: SerializedBytes<T>, val sig: DigitalSignature.WithKey) {
|
||||
/**
|
||||
* 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
|
||||
|
||||
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.
|
||||
* 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.
|
||||
* 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() {
|
||||
constructor(message: String?) : this(message, null)
|
||||
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.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.debug
|
||||
import org.slf4j.Logger
|
||||
import rx.Observable
|
||||
|
||||
@ -135,7 +136,9 @@ abstract class FlowLogic<out T> {
|
||||
if (shareParentSessions) {
|
||||
subLogic.sessionFlow = this
|
||||
}
|
||||
logger.debug { "Calling subflow: $subLogic" }
|
||||
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.
|
||||
subLogic.progressTracker?.currentStep = ProgressTracker.DONE
|
||||
return result
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.flows
|
||||
|
||||
import com.google.common.primitives.Primitives
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SingletonSerializeAsToken
|
||||
import java.lang.reflect.ParameterizedType
|
||||
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: 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())
|
||||
|
||||
// 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")
|
||||
|
||||
/**
|
||||
@ -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].
|
||||
*/
|
||||
// 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?>)
|
||||
|
||||
/**
|
||||
* 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?
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class AppContext(val attachments: List<SecureHash>) {
|
||||
// TODO: build a real [AttachmentsClassLoader] etc
|
||||
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.SecureHash
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import org.slf4j.Logger
|
||||
@ -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
|
||||
* 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) {
|
||||
companion object {
|
||||
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.StateMachineTransactionMapping
|
||||
import net.corda.core.node.services.Vault
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import rx.Observable
|
||||
import java.io.InputStream
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
|
||||
@CordaSerializable
|
||||
data class StateMachineInfo(
|
||||
val id: StateMachineRunId,
|
||||
val flowLogicClassName: String,
|
||||
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?
|
||||
)
|
||||
|
||||
@CordaSerializable
|
||||
sealed class StateMachineUpdate(val id: StateMachineRunId) {
|
||||
class Added(val stateMachineInfo: StateMachineInfo) : StateMachineUpdate(stateMachineInfo.id) {
|
||||
override fun toString() = "Added($id, ${stateMachineInfo.flowLogicClassName})"
|
||||
}
|
||||
|
||||
class Removed(id: StateMachineRunId) : StateMachineUpdate(id) {
|
||||
override fun toString() = "Removed($id)"
|
||||
}
|
||||
@ -107,12 +111,17 @@ interface CordaRPCOps : RPCOps {
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
fun uploadAttachment(jar: InputStream): SecureHash
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
// TODO: Remove this from the interface
|
||||
@Deprecated("This service will be removed in a future milestone")
|
||||
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].
|
||||
* 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.
|
||||
@ -153,6 +162,9 @@ interface CordaRPCOps : RPCOps {
|
||||
* Returns the [Party] with the given name as it's [Party.name]
|
||||
*/
|
||||
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 returnValue A [ListenableFuture] of the flow's return value.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class FlowHandle<A>(
|
||||
val id: StateMachineRunId,
|
||||
val progress: Observable<String>,
|
||||
|
@ -5,6 +5,7 @@ import com.google.common.util.concurrent.SettableFuture
|
||||
import net.corda.core.catch
|
||||
import net.corda.core.node.services.DEFAULT_SESSION_ID
|
||||
import net.corda.core.node.services.PartyInfo
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
|
||||
import net.corda.core.serialization.deserialize
|
||||
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
|
||||
* a session is established, use [DEFAULT_SESSION_ID].
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class TopicSession(val topic: String, val sessionID: Long = DEFAULT_SESSION_ID) {
|
||||
fun isBlank() = topic.isBlank() && sessionID == DEFAULT_SESSION_ID
|
||||
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
|
||||
* from a recipient. Using [Unit] can be ambiguous as it is similar to [Void] and so could mean no response.
|
||||
*/
|
||||
@CordaSerializable
|
||||
object Ack : DeserializeAsKotlinObjectDef
|
||||
|
@ -2,6 +2,7 @@ package net.corda.core.node
|
||||
|
||||
import net.corda.core.contracts.Attachment
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.FileNotFoundException
|
||||
@ -24,6 +25,7 @@ class AttachmentsClassLoader(attachments: List<Attachment>, parent: ClassLoader
|
||||
private val pathsToAttachments = HashMap<String, Attachment>()
|
||||
private val idsToAttachments = HashMap<SecureHash, Attachment>()
|
||||
|
||||
@CordaSerializable
|
||||
class OverlappingAttachments(val path: String) : Exception() {
|
||||
override fun toString() = "Multiple attachments define a file at path $path"
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import com.esotericsoftware.kryo.Kryo
|
||||
import net.corda.core.messaging.CordaRPCOps
|
||||
import net.corda.core.serialization.SerializationCustomization
|
||||
import java.util.function.Function
|
||||
|
||||
/**
|
||||
@ -40,14 +40,12 @@ abstract class CordaPluginRegistry(
|
||||
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
|
||||
* 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.
|
||||
* Optionally whitelist types for use in object serialization, as we lock down the types that can be serialized.
|
||||
*
|
||||
* For example, if you add a new [ContractState] it needs to be whitelisted. You can do that either by
|
||||
* adding the @CordaSerializable annotation or via this method.
|
||||
**
|
||||
* @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.node.services.ServiceInfo
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* Information for an advertised service including the service specific identity information.
|
||||
* The identity can be used in flows and is distinct from the Node's legalIdentity
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ServiceEntry(val info: ServiceInfo, val identity: Party)
|
||||
|
||||
/**
|
||||
* Info about a network node that acts on behalf of some form of contract party.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class NodeInfo(val address: SingleMessageRecipient,
|
||||
val legalIdentity: Party,
|
||||
val version: Version,
|
||||
var advertisedServices: List<ServiceEntry> = emptyList(),
|
||||
val physicalLocation: PhysicalLocation? = null) {
|
||||
init {
|
||||
|
@ -1,8 +1,10 @@
|
||||
package net.corda.core.node
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
||||
|
||||
/** A latitude/longitude pair. */
|
||||
@CordaSerializable
|
||||
data class WorldCoordinate(val latitude: Double, val longitude: Double) {
|
||||
init {
|
||||
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.
|
||||
* 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)
|
||||
|
||||
/**
|
||||
|
@ -9,6 +9,25 @@ import net.corda.core.transactions.SignedTransaction
|
||||
import java.security.KeyPair
|
||||
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
|
||||
* 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
|
||||
* state from being serialized in checkpoints.
|
||||
*/
|
||||
interface ServiceHub {
|
||||
interface ServiceHub : ServicesForResolution {
|
||||
val vaultService: VaultService
|
||||
val keyManagementService: KeyManagementService
|
||||
val identityService: IdentityService
|
||||
val storageService: StorageService
|
||||
val networkService: MessagingService
|
||||
override val storageService: StorageService
|
||||
val networkMapCache: NetworkMapCache
|
||||
val schedulerService: SchedulerService
|
||||
val clock: Clock
|
||||
@ -37,20 +55,29 @@ interface ServiceHub {
|
||||
// TODO: Make this take a single tx.
|
||||
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].
|
||||
*
|
||||
* @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)
|
||||
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> {
|
||||
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.crypto.SecureHash
|
||||
import java.io.InputStream
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* An attachment store records potentially large binary objects, identified by their hash.
|
||||
*/
|
||||
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
|
||||
* 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.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.messaging.MessageRecipients
|
||||
import net.corda.core.messaging.MessagingService
|
||||
import net.corda.core.messaging.SingleMessageRecipient
|
||||
import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.ServiceEntry
|
||||
import net.corda.core.randomOrNull
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import rx.Observable
|
||||
|
||||
/**
|
||||
@ -21,6 +20,7 @@ import rx.Observable
|
||||
*/
|
||||
interface NetworkMapCache {
|
||||
|
||||
@CordaSerializable
|
||||
sealed class MapChange(val node: NodeInfo) {
|
||||
class Added(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. */
|
||||
fun getNodeByLegalIdentityKey(compositeKey: CompositeKey): NodeInfo?
|
||||
|
||||
/** Look up all nodes advertising the service owned by [compositeKey] */
|
||||
fun getNodesByAdvertisedServiceIdentityKey(compositeKey: CompositeKey): List<NodeInfo> {
|
||||
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 */
|
||||
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
|
||||
* updates.
|
||||
@ -138,6 +144,7 @@ interface NetworkMapCache {
|
||||
fun runWithoutMapService()
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class NetworkCacheError : Exception() {
|
||||
/** Indicates a failure to deregister, because of a rejected request from the remote node */
|
||||
class DeregistrationFailed : NetworkCacheError()
|
||||
|
@ -1,5 +1,7 @@
|
||||
package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
|
||||
/**
|
||||
* 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
|
||||
* grouping identifier for nodes collectively running a distributed service.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ServiceInfo(val type: ServiceType, val name: String? = null) {
|
||||
companion object {
|
||||
fun parse(encoded: String): ServiceInfo {
|
||||
|
@ -1,10 +1,13 @@
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
sealed class ServiceType(val id: String) {
|
||||
init {
|
||||
// Enforce:
|
||||
@ -43,6 +46,7 @@ sealed class ServiceType(val id: String) {
|
||||
|
||||
fun isSubTypeOf(superType: ServiceType) = (id == superType.id) || id.startsWith(superType.id + ".")
|
||||
fun isNotary() = isSubTypeOf(notary)
|
||||
fun isValidatingNotary() = isNotary() && id.contains(".validating")
|
||||
|
||||
override fun hashCode(): Int = id.hashCode()
|
||||
override fun toString(): String = id.toString()
|
||||
|
@ -3,6 +3,7 @@ package net.corda.core.node.services
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.toFuture
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -32,13 +33,12 @@ val DEFAULT_SESSION_ID = 0L
|
||||
*
|
||||
* 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).
|
||||
* Relevant means they contain at least one of our pubkeys.
|
||||
*/
|
||||
class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
inline fun <reified T : ContractState> statesOfType() = states.filter { it.state.data is T } as List<StateAndRef<T>>
|
||||
@CordaSerializable
|
||||
class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* 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>>) {
|
||||
/** 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 }
|
||||
@ -81,6 +82,10 @@ class Vault(val states: List<StateAndRef<ContractState>>) {
|
||||
companion object {
|
||||
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.
|
||||
*/
|
||||
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.
|
||||
@ -125,25 +125,13 @@ interface VaultService {
|
||||
* 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.
|
||||
*/
|
||||
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>>
|
||||
|
||||
// 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 }
|
||||
}
|
||||
fun statesForRefs(refs: List<StateRef>): Map<StateRef, TransactionState<*>?>
|
||||
|
||||
/**
|
||||
* 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. */
|
||||
// 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.
|
||||
@ -173,7 +161,7 @@ interface VaultService {
|
||||
* 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].
|
||||
*/
|
||||
fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<UpgradedContract<*, *>>)
|
||||
fun authoriseContractUpgrade(stateAndRef: StateAndRef<*>, upgradedContractClass: Class<out UpgradedContract<*, *>>)
|
||||
|
||||
/**
|
||||
* Authorise a contract state upgrade.
|
||||
@ -212,11 +200,26 @@ interface VaultService {
|
||||
fun generateSpend(tx: TransactionBuilder,
|
||||
amount: Amount<Currency>,
|
||||
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 : DealState> VaultService.dealsWith(party: Party) = linearHeadsOfType<T>().values.filter {
|
||||
inline fun <reified T: ContractState> VaultService.unconsumedStates(): List<StateAndRef<T>> =
|
||||
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 }
|
||||
}
|
||||
|
||||
@ -261,12 +264,17 @@ interface FileUploader {
|
||||
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
|
||||
* 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.
|
||||
*/
|
||||
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.
|
||||
* 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
|
||||
|
||||
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
|
||||
val attachments: AttachmentStorage
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("This service will be removed in a future milestone")
|
||||
val uploaders: List<FileUploader>
|
||||
|
@ -2,8 +2,10 @@ package net.corda.core.node.services
|
||||
|
||||
import net.corda.core.crypto.SecureHash
|
||||
import net.corda.core.flows.StateMachineRunId
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import rx.Observable
|
||||
|
||||
@CordaSerializable
|
||||
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.crypto.Party
|
||||
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
|
||||
@ -15,6 +16,7 @@ interface UniquenessProvider {
|
||||
fun commit(states: List<StateRef>, txId: SecureHash, callerIdentity: Party)
|
||||
|
||||
/** Specifies the consuming transaction for every conflicting state */
|
||||
@CordaSerializable
|
||||
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
|
||||
* find out where exactly they were spent.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class ConsumingTx(val id: SecureHash, val inputIndex: Int, val requestingParty: Party)
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class UniquenessException(val error: UniquenessProvider.Conflict) : Exception()
|
||||
|
@ -1,5 +1,6 @@
|
||||
package net.corda.core.schemas
|
||||
|
||||
import io.requery.Persistable
|
||||
import net.corda.core.contracts.ContractState
|
||||
import net.corda.core.contracts.StateRef
|
||||
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
|
||||
* [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.
|
||||
@ -62,5 +63,9 @@ data class PersistentStateRef(
|
||||
var index: Int?
|
||||
) : Serializable {
|
||||
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)
|
||||
}
|
||||
|
@ -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
|
||||
* functionality to Java, but it won't arrive for a few years yet!
|
||||
*/
|
||||
@CordaSerializable
|
||||
open class OpaqueBytes(val bytes: ByteArray) {
|
||||
init {
|
||||
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
|
||||
|
||||
import co.paralleluniverse.fibers.Fiber
|
||||
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.*
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.esotericsoftware.kryo.io.Output
|
||||
import com.esotericsoftware.kryo.serializers.JavaSerializer
|
||||
import com.esotericsoftware.kryo.serializers.MapSerializer
|
||||
import de.javakaffee.kryoserializers.ArraysAsListSerializer
|
||||
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
|
||||
import de.javakaffee.kryoserializers.guava.*
|
||||
import com.esotericsoftware.kryo.util.MapReferenceResolver
|
||||
import net.corda.core.contracts.*
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.node.AttachmentsClassLoader
|
||||
import net.corda.core.node.services.AttachmentStorage
|
||||
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 net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
|
||||
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
|
||||
import org.objenesis.strategy.StdInstantiatorStrategy
|
||||
import java.io.*
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.security.PublicKey
|
||||
import java.security.spec.InvalidKeySpecException
|
||||
import java.time.Instant
|
||||
import java.util.*
|
||||
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,
|
||||
* isn't usable beyond the prototyping stage. But that's fine: we can revisit serialisation technologies later after
|
||||
* 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.
|
||||
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]
|
||||
* 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.
|
||||
val hash: SecureHash by lazy { bytes.sha256() }
|
||||
|
||||
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.
|
||||
fun <T : Any> ByteArray.deserialize(kryo: Kryo = THREAD_LOCAL_KRYO.get()): T {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return kryo.readClassAndObject(Input(this)) as 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")
|
||||
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)
|
||||
}
|
||||
|
||||
// The more specific deserialize version results in the bytes being cached, which is faster.
|
||||
@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
|
||||
@ -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
|
||||
* 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()
|
||||
Output(stream).use {
|
||||
it.writeBytes(KryoHeaderV0_1.bytes)
|
||||
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 */
|
||||
@CordaSerializable
|
||||
class MissingAttachmentsException(val ids: List<SecureHash>) : Exception()
|
||||
|
||||
/** 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) {}
|
||||
}
|
||||
|
||||
fun createKryo(k: Kryo = Kryo()): Kryo {
|
||||
return k.apply {
|
||||
// Allow any class to be deserialized (this is insecure but for prototyping we don't care)
|
||||
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())
|
||||
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
|
||||
fun createInternalKryo(k: Kryo = CordaKryo(makeNoWhitelistClassResolver())): Kryo {
|
||||
return DefaultKryoCustomizer.customize(k)
|
||||
}
|
||||
|
||||
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 avoid it here.
|
||||
register(Kryo::class,
|
||||
read = { kryo, input -> createKryo((Fiber.getFiberSerializer() as KryoSerializer).kryo) },
|
||||
write = { kryo, output, obj -> }
|
||||
)
|
||||
/**
|
||||
* We need to disable whitelist checking during calls from our Kryo code to register a serializer, since it checks
|
||||
* for existing registrations and then will enter our [CordaClassResolver.getRegistration] method.
|
||||
*/
|
||||
open class CordaKryo(classResolver: ClassResolver) : Kryo(classResolver, MapReferenceResolver()) {
|
||||
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)
|
||||
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
|
||||
register(Instant::class.java, ReferencesAwareJavaSerializer)
|
||||
override fun register(type: Class<*>?, id: Int): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(type, id)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
|
||||
// Using a custom serializer for compactness
|
||||
register(CompositeKey.Node::class.java, CompositeKeyNodeSerializer)
|
||||
register(CompositeKey.Leaf::class.java, CompositeKeyLeafSerializer)
|
||||
override fun register(type: Class<*>?, serializer: Serializer<*>?): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
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
|
||||
// constructors be invoked (typically for lazy members).
|
||||
register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class))
|
||||
|
||||
// This class has special handling.
|
||||
register(WireTransaction::class.java, WireTransactionSerializer)
|
||||
|
||||
// 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>()
|
||||
override fun register(registration: Registration?): Registration {
|
||||
(classResolver as? CordaClassResolver)?.disableWhitelist()
|
||||
try {
|
||||
return super.register(registration)
|
||||
} finally {
|
||||
(classResolver as? CordaClassResolver)?.enableWhitelist()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -554,3 +553,32 @@ object OrderedSerializer : Serializer<HashMap<Any, Any>>() {
|
||||
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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface SerializeAsToken {
|
||||
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
|
||||
* (when deserialized) via just the class name.
|
||||
*/
|
||||
@CordaSerializable
|
||||
data class SingletonSerializationToken private constructor(private val className: String) : SerializationToken {
|
||||
|
||||
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) {
|
||||
override fun generate(random: SourceOfRandomness, status: GenerationStatus): Party {
|
||||
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) {
|
||||
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." }
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
other is BaseTransaction &&
|
||||
notary == other.notary &&
|
||||
mustSign == other.mustSign &&
|
||||
type == other.type &&
|
||||
timestamp == other.timestamp
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other === this) return true
|
||||
return other is BaseTransaction &&
|
||||
notary == other.notary &&
|
||||
mustSign == other.mustSign &&
|
||||
type == other.type &&
|
||||
timestamp == other.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.Party
|
||||
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:
|
||||
@ -16,6 +17,7 @@ import net.corda.core.crypto.SecureHash
|
||||
*
|
||||
* All the above refer to inputs using a (txhash, output index) pair.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class LedgerTransaction(
|
||||
/** The resolved input states which will be consumed/invalidated by the execution of this transaction. */
|
||||
override val inputs: List<StateAndRef<*>>,
|
||||
|
@ -2,11 +2,10 @@ package net.corda.core.transactions
|
||||
|
||||
import net.corda.core.contracts.*
|
||||
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.extendKryoHash
|
||||
import net.corda.core.serialization.serialize
|
||||
import java.util.*
|
||||
|
||||
fun <T : Any> serializedHash(x: T): SecureHash {
|
||||
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.
|
||||
* Property traversableList assures that we always calculate hashes in the same order, lets us define which
|
||||
* fields of WireTransaction will be included in id calculation or partial merkle tree building.
|
||||
* Implemented by [WireTransaction] and [FilteredLeaves]. A TraversableTransaction allows you to iterate
|
||||
* over the flattened components of the underlying transaction structure, taking into account that some
|
||||
* 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 {
|
||||
val inputs: List<StateRef>
|
||||
@ -29,30 +32,43 @@ interface TraversableTransaction {
|
||||
val timestamp: Timestamp?
|
||||
|
||||
/**
|
||||
* Traversing transaction fields with a list function over transaction contents. Used for leaves hashes calculation
|
||||
* and user provided filtering and checking of filtered transaction.
|
||||
* Returns a flattened list of all the components that are present in the transaction, in the following order:
|
||||
*
|
||||
* - 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
|
||||
*/
|
||||
// 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
|
||||
// torn-off transaction and id calculation.
|
||||
val traversableList: List<Any>
|
||||
val availableComponents: List<Any>
|
||||
get() {
|
||||
val traverseList = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
|
||||
if (notary != null) traverseList.add(notary!!)
|
||||
traverseList.addAll(mustSign)
|
||||
if (type != null) traverseList.add(type!!)
|
||||
if (timestamp != null) traverseList.add(timestamp!!)
|
||||
return traverseList
|
||||
// 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
|
||||
// torn-off transaction and id calculation.
|
||||
val result = mutableListOf(inputs, attachments, outputs, commands).flatten().toMutableList()
|
||||
notary?.let { result += it }
|
||||
result.addAll(mustSign)
|
||||
type?.let { result += it }
|
||||
timestamp?.let { result += it }
|
||||
return result
|
||||
}
|
||||
|
||||
// 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
|
||||
* field from WireTransaction can be used in PartialMerkleTree calculation.
|
||||
* field from [WireTransaction] can be used in [PartialMerkleTree] calculation.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class FilteredLeaves(
|
||||
override val inputs: List<StateRef>,
|
||||
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.
|
||||
*/
|
||||
fun checkWithFun(checkingFun: (Any) -> Boolean): Boolean {
|
||||
val checkList = traversableList.map { checkingFun(it) }
|
||||
val checkList = availableComponents.map { checkingFun(it) }
|
||||
return (!checkList.isEmpty()) && checkList.all { true }
|
||||
}
|
||||
}
|
||||
@ -84,6 +100,7 @@ class FilteredLeaves(
|
||||
* @param filteredLeaves Leaves included in a filtered transaction.
|
||||
* @param partialMerkleTree Merkle branch needed to verify filteredLeaves.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class FilteredTransaction private constructor(
|
||||
val rootHash: SecureHash,
|
||||
val filteredLeaves: FilteredLeaves,
|
||||
@ -99,8 +116,8 @@ class FilteredTransaction private constructor(
|
||||
filtering: (Any) -> Boolean
|
||||
): FilteredTransaction {
|
||||
val filteredLeaves = wtx.filterWithFun(filtering)
|
||||
val merkleTree = wtx.getMerkleTree()
|
||||
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.calculateLeavesHashes())
|
||||
val merkleTree = wtx.merkleTree
|
||||
val pmt = PartialMerkleTree.build(merkleTree, filteredLeaves.availableComponentHashes)
|
||||
return FilteredTransaction(merkleTree.hash, filteredLeaves, pmt)
|
||||
}
|
||||
}
|
||||
@ -110,17 +127,9 @@ class FilteredTransaction private constructor(
|
||||
*/
|
||||
@Throws(MerkleTreeException::class)
|
||||
fun verify(): Boolean {
|
||||
val hashes: List<SecureHash> = filteredLeaves.calculateLeavesHashes()
|
||||
val hashes: List<SecureHash> = filteredLeaves.availableComponentHashes
|
||||
if (hashes.isEmpty())
|
||||
throw MerkleTreeException("Transaction without included leaves.")
|
||||
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.signWithECDSA
|
||||
import net.corda.core.node.ServiceHub
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.SerializedBytes
|
||||
import java.security.KeyPair
|
||||
import java.security.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.
|
||||
*/
|
||||
data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
|
||||
val sigs: List<DigitalSignature.WithKey>,
|
||||
override val id: SecureHash
|
||||
val sigs: List<DigitalSignature.WithKey>
|
||||
) : NamedByHash {
|
||||
init {
|
||||
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.
|
||||
|
||||
/** Lazily calculated access to the deserialised/hashed transaction data. */
|
||||
val tx: WireTransaction by lazy {
|
||||
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
|
||||
}
|
||||
val tx: WireTransaction by lazy { WireTransaction.deserialize(txBits) }
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
override fun toString(): String {
|
||||
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.
|
||||
*/
|
||||
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()}")
|
||||
}
|
||||
val wtx = toWireTransaction()
|
||||
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs), wtx.id)
|
||||
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs))
|
||||
}
|
||||
|
||||
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.SecureHash
|
||||
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.THREAD_LOCAL_KRYO
|
||||
import net.corda.core.serialization.deserialize
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.serialization.threadLocalP2PKryo
|
||||
import net.corda.core.utilities.Emoji
|
||||
import java.security.PublicKey
|
||||
|
||||
@ -42,10 +42,10 @@ class WireTransaction(
|
||||
@Volatile @Transient private var cachedBytes: SerializedBytes<WireTransaction>? = null
|
||||
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 {
|
||||
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)
|
||||
wtx.cachedBytes = data
|
||||
return wtx
|
||||
@ -70,7 +70,7 @@ class WireTransaction(
|
||||
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
|
||||
*/
|
||||
@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.
|
||||
val authenticatedArgs = commands.map {
|
||||
val parties = it.signers.mapNotNull { pk -> services.identityService.partyFromKey(pk) }
|
||||
@ -94,9 +94,7 @@ class WireTransaction(
|
||||
/**
|
||||
* Builds whole Merkle tree for a transaction.
|
||||
*/
|
||||
fun getMerkleTree(): MerkleTree {
|
||||
return MerkleTree.getMerkleTree(calculateLeavesHashes())
|
||||
}
|
||||
val merkleTree: MerkleTree by lazy { MerkleTree.getMerkleTree(availableComponentHashes) }
|
||||
|
||||
/**
|
||||
* Construction of partial transaction from WireTransaction based on filtering.
|
||||
@ -119,9 +117,9 @@ class WireTransaction(
|
||||
|
||||
override fun toString(): String {
|
||||
val buf = StringBuilder()
|
||||
buf.appendln("Transaction $id:")
|
||||
buf.appendln("Transaction:")
|
||||
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 (attachment in attachments) buf.appendln("${Emoji.paperclip}ATTACHMENT: $attachment")
|
||||
return buf.toString()
|
||||
|
@ -1,5 +1,7 @@
|
||||
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.
|
||||
*/
|
||||
@ -7,15 +9,18 @@ object Emoji {
|
||||
// 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")) }
|
||||
|
||||
const val CODE_SANTA_CLAUS = "\ud83c\udf85"
|
||||
const val CODE_DIAMOND = "\ud83d\udd37"
|
||||
const val CODE_BAG_OF_CASH = "\ud83d\udcb0"
|
||||
const val CODE_NEWSPAPER = "\ud83d\udcf0"
|
||||
const val CODE_RIGHT_ARROW = "\u27a1\ufe0f"
|
||||
const val CODE_LEFT_ARROW = "\u2b05\ufe0f"
|
||||
const val CODE_GREEN_TICK = "\u2705"
|
||||
const val CODE_PAPERCLIP = "\ud83d\udcce"
|
||||
const val CODE_COOL_GUY = "\ud83d\ude0e"
|
||||
@JvmStatic val CODE_SANTA_CLAUS: String = codePointsString(0x1F385)
|
||||
@JvmStatic val CODE_DIAMOND: String = codePointsString(0x1F537)
|
||||
@JvmStatic val CODE_BAG_OF_CASH: String = codePointsString(0x1F4B0)
|
||||
@JvmStatic val CODE_NEWSPAPER: String = codePointsString(0x1F4F0)
|
||||
@JvmStatic val CODE_RIGHT_ARROW: String = codePointsString(0x27A1, 0xFE0F)
|
||||
@JvmStatic val CODE_LEFT_ARROW: String = codePointsString(0x2B05, 0xFE0F)
|
||||
@JvmStatic val CODE_GREEN_TICK: String = codePointsString(0x2705)
|
||||
@JvmStatic val CODE_PAPERCLIP: String = codePointsString(0x1F4CE)
|
||||
@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
|
||||
@ -31,6 +36,7 @@ object Emoji {
|
||||
val leftArrow: String get() = if (emojiMode.get() != null) "$CODE_LEFT_ARROW " 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 books: String get() = if (emojiMode.get() != null) "$CODE_BOOKS " else ""
|
||||
|
||||
inline fun <T> renderIfSupported(body: () -> T): T {
|
||||
emojiMode.set(this) // Could be any object.
|
||||
@ -55,4 +61,5 @@ object Emoji {
|
||||
emojiMode.set(null)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package net.corda.core.utilities
|
||||
|
||||
import net.corda.core.TransientProperty
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
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
|
||||
* using the [Observable] subscribeOn call.
|
||||
*/
|
||||
@CordaSerializable
|
||||
class ProgressTracker(vararg steps: Step) {
|
||||
@CordaSerializable
|
||||
sealed class Change {
|
||||
class Position(val tracker: ProgressTracker, val newStep: Step) : Change() {
|
||||
override fun toString() = newStep.label
|
||||
@ -48,6 +51,7 @@ class ProgressTracker(vararg steps: Step) {
|
||||
}
|
||||
|
||||
/** The superclass of all step objects. */
|
||||
@CordaSerializable
|
||||
open class Step(open val label: String) {
|
||||
open val changes: Observable<Change> get() = Observable.empty()
|
||||
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. */
|
||||
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
|
||||
set(value) {
|
||||
@ -81,6 +85,7 @@ class ProgressTracker(vararg steps: Step) {
|
||||
// This field won't be serialized.
|
||||
private val _changes by TransientProperty { PublishSubject.create<Change>() }
|
||||
|
||||
@CordaSerializable
|
||||
private data class Child(val tracker: ProgressTracker, @Transient val subscription: Subscription?)
|
||||
|
||||
private val childProgressTrackers = HashMap<Step, Child>()
|
||||
@ -105,27 +110,26 @@ class ProgressTracker(vararg steps: Step) {
|
||||
var currentStep: Step
|
||||
get() = steps[stepIndex]
|
||||
set(value) {
|
||||
if (currentStep != value) {
|
||||
check(currentStep != DONE) { "Cannot rewind a progress tracker once it reaches the done state" }
|
||||
check(!hasEnded) { "Cannot rewind a progress tracker once it has ended" }
|
||||
if (currentStep == value) return
|
||||
|
||||
val index = steps.indexOf(value)
|
||||
require(index != -1)
|
||||
val index = steps.indexOf(value)
|
||||
require(index != -1)
|
||||
|
||||
if (index < stepIndex) {
|
||||
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
|
||||
// through, in preparation for moving through them again.
|
||||
for (i in stepIndex downTo index) {
|
||||
removeChildProgressTracker(steps[i])
|
||||
}
|
||||
if (index < stepIndex) {
|
||||
// We are going backwards: unlink and unsubscribe from any child nodes that we're rolling back
|
||||
// through, in preparation for moving through them again.
|
||||
for (i in stepIndex downTo index) {
|
||||
removeChildProgressTracker(steps[i])
|
||||
}
|
||||
|
||||
curChangeSubscription?.unsubscribe()
|
||||
stepIndex = index
|
||||
_changes.onNext(Change.Position(this, steps[index]))
|
||||
curChangeSubscription = currentStep.changes.subscribe { _changes.onNext(it) }
|
||||
|
||||
if (currentStep == DONE) _changes.onCompleted()
|
||||
}
|
||||
|
||||
curChangeSubscription?.unsubscribe()
|
||||
stepIndex = index
|
||||
_changes.onNext(Change.Position(this, steps[index]))
|
||||
curChangeSubscription = currentStep.changes.subscribe( { _changes.onNext(it) }, { _changes.onError(it) })
|
||||
|
||||
if (currentStep == DONE) _changes.onCompleted()
|
||||
}
|
||||
|
||||
/** Returns the current step, descending into children to find the deepest step we are up to. */
|
||||
@ -149,6 +153,15 @@ class ProgressTracker(vararg steps: 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 */
|
||||
var parent: ProgressTracker? = null
|
||||
private set
|
||||
@ -195,6 +208,9 @@ class ProgressTracker(vararg steps: Step) {
|
||||
* if a step changed its label or rendering).
|
||||
*/
|
||||
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.FlowLogic
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
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 signatures = if (participants == me) {
|
||||
listOf(getNotarySignature(stx))
|
||||
getNotarySignatures(stx)
|
||||
} else {
|
||||
collectSignatures(participants - me, stx)
|
||||
}
|
||||
@ -87,7 +89,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
val allPartySignedTx = stx + participantSignatures
|
||||
|
||||
val allSignatures = participantSignatures + getNotarySignature(allPartySignedTx)
|
||||
val allSignatures = participantSignatures + getNotarySignatures(allPartySignedTx)
|
||||
parties.forEach { send(it, allSignatures) }
|
||||
|
||||
return allSignatures
|
||||
@ -105,7 +107,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.WithKey {
|
||||
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
progressTracker.currentStep = NOTARY
|
||||
try {
|
||||
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,
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<Unit>() {
|
||||
override val progressTracker: ProgressTracker = tracker()) : FlowLogic<Void?>() {
|
||||
companion object {
|
||||
object VERIFYING : ProgressTracker.Step("Verifying state replacement proposal")
|
||||
object APPROVING : ProgressTracker.Step("State replacement approved")
|
||||
@ -126,7 +130,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
|
||||
@Suspendable
|
||||
@Throws(StateReplacementException::class)
|
||||
override fun call() {
|
||||
override fun call(): Void? {
|
||||
progressTracker.currentStep = VERIFYING
|
||||
val maybeProposal: UntrustworthyData<Proposal<T>> = receive(otherSide)
|
||||
val stx: SignedTransaction = maybeProposal.unwrap {
|
||||
@ -135,6 +139,7 @@ abstract class AbstractStateReplacementFlow {
|
||||
it.stx
|
||||
}
|
||||
approve(stx)
|
||||
return null
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
|
@ -3,6 +3,7 @@ package net.corda.flows
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
|
||||
|
||||
@ -17,6 +18,7 @@ import net.corda.core.transactions.SignedTransaction
|
||||
*/
|
||||
class BroadcastTransactionFlow(val notarisedTransaction: SignedTransaction,
|
||||
val participants: Set<Party>) : FlowLogic<Unit>() {
|
||||
@CordaSerializable
|
||||
data class NotifyTxRequest(val tx: SignedTransaction)
|
||||
|
||||
@Suspendable
|
||||
|
@ -30,10 +30,11 @@ object ContractUpgradeFlow {
|
||||
val command = commandData.value as UpgradeCommand
|
||||
val participants: Set<CompositeKey> = input.participants.toSet()
|
||||
val keysThatSigned: Set<CompositeKey> = commandData.signers.toSet()
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val upgradedContract = command.upgradedContractClass.newInstance() as UpgradedContract<ContractState, *>
|
||||
requireThat {
|
||||
"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)
|
||||
"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(
|
||||
stateRef: StateAndRef<OldState>,
|
||||
upgradedContractClass: Class<UpgradedContract<OldState, NewState>>
|
||||
upgradedContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
): TransactionBuilder {
|
||||
val contractUpgrade = upgradedContractClass.newInstance()
|
||||
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>,
|
||||
newContractClass: Class<UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
newContractClass: Class<out UpgradedContract<OldState, NewState>>
|
||||
) : AbstractStateReplacementFlow.Instigator<OldState, NewState, Class<out UpgradedContract<OldState, NewState>>>(originalState, newContractClass) {
|
||||
|
||||
override fun assembleTx(): Pair<SignedTransaction, Iterable<CompositeKey>> {
|
||||
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
|
||||
@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.
|
||||
val stx = subFlow(FetchTransactionsFlow(setOf(proposal.stateRef.txhash), otherSide)).fromDisk.singleOrNull()
|
||||
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.SecureHash
|
||||
import net.corda.core.crypto.sha256
|
||||
import java.io.ByteArrayInputStream
|
||||
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 convert(wire: ByteArray): Attachment {
|
||||
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 convert(wire: ByteArray): Attachment = ByteArrayAttachment(wire)
|
||||
|
||||
override fun maybeWriteToDisk(downloaded: List<Attachment>) {
|
||||
for (attachment in downloaded) {
|
||||
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.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.utilities.UntrustworthyData
|
||||
import net.corda.core.utilities.unwrap
|
||||
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 otherSide: Party) : FlowLogic<FetchDataFlow.Result<T>>() {
|
||||
|
||||
@CordaSerializable
|
||||
class DownloadedVsRequestedDataMismatch(val requested: SecureHash, val got: SecureHash) : IllegalArgumentException()
|
||||
|
||||
@CordaSerializable
|
||||
class DownloadedVsRequestedSizeMismatch(val requested: Int, val got: Int) : IllegalArgumentException()
|
||||
|
||||
class HashNotFound(val requested: SecureHash) : FlowException()
|
||||
|
||||
@CordaSerializable
|
||||
data class Request(val hashes: List<SecureHash>)
|
||||
|
||||
@CordaSerializable
|
||||
data class Result<out T : NamedByHash>(val fromDisk: List<T>, val downloaded: List<T>)
|
||||
|
||||
@Suspendable
|
||||
|
@ -74,8 +74,8 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
return stxnsAndParties.map { pair ->
|
||||
val stx = pair.first
|
||||
val notarised = if (needsNotarySignature(stx)) {
|
||||
val notarySig = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySig
|
||||
val notarySignatures = subFlow(NotaryFlow.Client(stx))
|
||||
stx + notarySignatures
|
||||
} else {
|
||||
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 {
|
||||
val notaryKey = stx.tx.notary?.owningKey
|
||||
val signers = stx.sigs.map { it.by }.toSet()
|
||||
@ -117,7 +122,7 @@ class FinalityFlow(val transactions: Iterable<SignedTransaction>,
|
||||
// Load and verify each transaction.
|
||||
return sorted.map { stx ->
|
||||
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 ltx = wtx.toLedgerTransaction(augmentedLookup)
|
||||
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
|
||||
|
||||
import co.paralleluniverse.fibers.Suspendable
|
||||
import net.corda.core.crypto.DigitalSignature
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SignedData
|
||||
import net.corda.core.crypto.signWithECDSA
|
||||
import net.corda.core.contracts.StateRef
|
||||
import net.corda.core.contracts.Timestamp
|
||||
import net.corda.core.crypto.*
|
||||
import net.corda.core.flows.FlowException
|
||||
import net.corda.core.flows.FlowLogic
|
||||
import net.corda.core.node.services.TimestampChecker
|
||||
import net.corda.core.node.services.UniquenessException
|
||||
import net.corda.core.node.services.UniquenessProvider
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.serialization.serialize
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.ProgressTracker
|
||||
import net.corda.core.utilities.unwrap
|
||||
|
||||
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.
|
||||
*
|
||||
* In case of a single-node or Raft notary, the flow will return a single signature. For the BFT notary multiple
|
||||
* signatures will be returned – one from each replica that accepted the input state commit.
|
||||
*
|
||||
* @throws NotaryException in case the any of the inputs to the transaction have been consumed
|
||||
* by another transaction or the timestamp is invalid.
|
||||
*/
|
||||
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())
|
||||
|
||||
companion object {
|
||||
@ -39,7 +41,7 @@ object NotaryFlow {
|
||||
|
||||
@Suspendable
|
||||
@Throws(NotaryException::class)
|
||||
override fun call(): DigitalSignature.WithKey {
|
||||
override fun call(): List<DigitalSignature.WithKey> {
|
||||
progressTracker.currentStep = REQUESTING
|
||||
val wtx = stx.tx
|
||||
notaryParty = wtx.notary ?: throw IllegalStateException("Transaction does not specify a Notary")
|
||||
@ -52,8 +54,14 @@ object NotaryFlow {
|
||||
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 {
|
||||
sendAndReceive<DigitalSignature.WithKey>(notaryParty, SignRequest(stx))
|
||||
sendAndReceive<List<DigitalSignature.WithKey>>(notaryParty, payload)
|
||||
} catch (e: NotaryException) {
|
||||
if (e.error is NotaryError.Conflict) {
|
||||
e.error.conflict.verified()
|
||||
@ -61,9 +69,9 @@ object NotaryFlow {
|
||||
throw e
|
||||
}
|
||||
|
||||
return response.unwrap { sig ->
|
||||
validateSignature(sig, stx.id.bytes)
|
||||
sig
|
||||
return response.unwrap { signatures ->
|
||||
signatures.forEach { validateSignature(it, stx.id.bytes) }
|
||||
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.
|
||||
*
|
||||
* Extend this class, overriding _beforeCommit_ to add custom transaction processing/validation logic.
|
||||
*
|
||||
* TODO: the notary service should only be able to see timestamp commands and inputs
|
||||
* Additional transaction validation logic can be added when implementing [receiveAndVerifyTx].
|
||||
*/
|
||||
open class Service(val otherSide: Party,
|
||||
val timestampChecker: TimestampChecker,
|
||||
val uniquenessProvider: UniquenessProvider) : FlowLogic<Unit>() {
|
||||
|
||||
// See AbstractStateReplacementFlow.Acceptor for why it's Void?
|
||||
abstract class Service(val otherSide: Party,
|
||||
val timestampChecker: TimestampChecker,
|
||||
val uniquenessProvider: UniquenessProvider) : FlowLogic<Void?>() {
|
||||
@Suspendable
|
||||
override fun call() {
|
||||
val stx = receive<SignRequest>(otherSide).unwrap { it.tx }
|
||||
val wtx = stx.tx
|
||||
|
||||
validateTimestamp(wtx)
|
||||
beforeCommit(stx)
|
||||
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())
|
||||
override fun call(): Void? {
|
||||
val (id, inputs, timestamp) = receiveAndVerifyTx()
|
||||
validateTimestamp(timestamp)
|
||||
commitInputStates(inputs, id)
|
||||
signAndSendResponse(id)
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* No pre-commit processing is done. 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).
|
||||
* Implement custom logic to receive the transaction to notarise, and perform verification based on validity and
|
||||
* privacy requirements.
|
||||
*/
|
||||
@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
|
||||
* 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 {
|
||||
uniquenessProvider.commit(tx.inputs, tx.id, otherSide)
|
||||
uniquenessProvider.commit(inputs, txId, otherSide)
|
||||
} catch (e: UniquenessException) {
|
||||
val conflicts = tx.inputs.filterIndexed { i, stateRef ->
|
||||
val conflicts = inputs.filterIndexed { i, 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()) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
private fun notaryException(tx: WireTransaction, e: UniquenessException): NotaryException {
|
||||
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
|
||||
val conflictData = e.error.serialize()
|
||||
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() {
|
||||
override fun toString() = "${super.toString()}: Error response from Notary - $error"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
sealed class NotaryError {
|
||||
class Conflict(val tx: WireTransaction, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction ${tx.id} have been used in another transaction"
|
||||
class Conflict(val txId: SecureHash, val conflict: SignedData<UniquenessProvider.Conflict>) : NotaryError() {
|
||||
override fun toString() = "One or more input states for transaction $txId have been used in another transaction"
|
||||
}
|
||||
|
||||
/** Thrown if the time specified in the timestamp command is outside the allowed tolerance */
|
||||
class TimestampInvalid : 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() {
|
||||
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.flows.FlowLogic
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.LedgerTransaction
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -67,6 +68,7 @@ class ResolveTransactionsFlow(private val txHashes: Set<SecureHash>,
|
||||
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class ExcessivelyLargeTransactionGraph() : Exception()
|
||||
|
||||
// Transactions to verify after the dependencies.
|
||||
|
@ -3,10 +3,12 @@ package net.corda.flows
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import net.corda.core.messaging.*
|
||||
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.
|
||||
*/
|
||||
@CordaSerializable
|
||||
interface ServiceRequestMessage {
|
||||
val sessionID: Long
|
||||
val replyTo: SingleMessageRecipient
|
||||
|
@ -10,6 +10,7 @@ import net.corda.core.node.NodeInfo
|
||||
import net.corda.core.node.recordTransactions
|
||||
import net.corda.core.node.services.ServiceType
|
||||
import net.corda.core.seconds
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import net.corda.core.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.TransactionBuilder
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
@ -31,18 +32,22 @@ import java.security.KeyPair
|
||||
*/
|
||||
object TwoPartyDealFlow {
|
||||
|
||||
@CordaSerializable
|
||||
class DealMismatchException(val expectedDeal: ContractState, val actualDeal: ContractState) : Exception() {
|
||||
override fun toString() = "The submitted deal didn't match the expected: $expectedDeal vs $actualDeal"
|
||||
}
|
||||
|
||||
@CordaSerializable
|
||||
class DealRefMismatchException(val expectedDeal: StateRef, val actualDeal: StateRef) : Exception() {
|
||||
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.
|
||||
@CordaSerializable
|
||||
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
|
||||
@ -139,9 +144,9 @@ object TwoPartyDealFlow {
|
||||
// These two steps could be done in parallel, in theory. Our framework doesn't support that yet though.
|
||||
val ourSignature = computeOurSignature(stx)
|
||||
val allPartySignedTx = stx + ourSignature
|
||||
val notarySignature = getNotarySignature(allPartySignedTx)
|
||||
val notarySignatures = getNotarySignatures(allPartySignedTx)
|
||||
|
||||
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignature)
|
||||
val fullySigned = sendSignatures(allPartySignedTx, ourSignature, notarySignatures)
|
||||
|
||||
progressTracker.currentStep = RECORDING
|
||||
|
||||
@ -161,7 +166,7 @@ object TwoPartyDealFlow {
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun getNotarySignature(stx: SignedTransaction): DigitalSignature.WithKey {
|
||||
private fun getNotarySignatures(stx: SignedTransaction): List<DigitalSignature.WithKey> {
|
||||
progressTracker.currentStep = NOTARY
|
||||
return subFlow(NotaryFlow.Client(stx))
|
||||
}
|
||||
@ -173,13 +178,13 @@ object TwoPartyDealFlow {
|
||||
|
||||
@Suspendable
|
||||
private fun sendSignatures(allPartySignedTx: SignedTransaction, ourSignature: DigitalSignature.WithKey,
|
||||
notarySignature: DigitalSignature.WithKey): SignedTransaction {
|
||||
notarySignatures: List<DigitalSignature.WithKey>): SignedTransaction {
|
||||
progressTracker.currentStep = SENDING_SIGS
|
||||
val fullySigned = allPartySignedTx + notarySignature
|
||||
val fullySigned = allPartySignedTx + notarySignatures
|
||||
|
||||
logger.trace { "Built finished transaction, sending back to other party!" }
|
||||
|
||||
send(otherParty, SignaturesFromPrimary(ourSignature, notarySignature))
|
||||
send(otherParty, SignaturesFromPrimary(ourSignature, notarySignatures))
|
||||
return fullySigned
|
||||
}
|
||||
}
|
||||
@ -217,7 +222,7 @@ object TwoPartyDealFlow {
|
||||
|
||||
logger.trace { "Got signatures from other party, verifying ... " }
|
||||
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySig
|
||||
val fullySigned = stx + signatures.sellerSig + signatures.notarySigs
|
||||
fullySigned.verifySignatures()
|
||||
|
||||
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>>
|
||||
}
|
||||
|
||||
|
||||
@CordaSerializable
|
||||
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
|
||||
// to have one.
|
||||
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.transactions.SignedTransaction
|
||||
import net.corda.core.transactions.WireTransaction
|
||||
import net.corda.core.utilities.unwrap
|
||||
import java.security.SignatureException
|
||||
|
||||
/**
|
||||
@ -19,21 +20,18 @@ class ValidatingNotaryFlow(otherSide: Party,
|
||||
timestampChecker: TimestampChecker,
|
||||
uniquenessProvider: 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
|
||||
override fun beforeCommit(stx: SignedTransaction) {
|
||||
try {
|
||||
checkSignatures(stx)
|
||||
val wtx = stx.tx
|
||||
resolveTransaction(wtx)
|
||||
wtx.toLedgerTransaction(serviceHub).verify()
|
||||
} catch (e: Exception) {
|
||||
when (e) {
|
||||
is TransactionVerificationException -> NotaryException(NotaryError.TransactionInvalid(e.toString()))
|
||||
is SignatureException -> throw NotaryException(NotaryError.SignaturesInvalid(e.toString()))
|
||||
else -> throw e
|
||||
}
|
||||
}
|
||||
override fun receiveAndVerifyTx(): TransactionParts {
|
||||
val stx = receive<SignedTransaction>(otherSide).unwrap { it }
|
||||
checkSignatures(stx)
|
||||
val wtx = stx.tx
|
||||
validateTransaction(wtx)
|
||||
return TransactionParts(wtx.id, wtx.inputs, wtx.timestamp)
|
||||
}
|
||||
|
||||
private fun checkSignatures(stx: SignedTransaction) {
|
||||
@ -45,7 +43,19 @@ class ValidatingNotaryFlow(otherSide: Party,
|
||||
}
|
||||
|
||||
@Suspendable
|
||||
private fun resolveTransaction(wtx: WireTransaction) {
|
||||
subFlow(ResolveTransactionsFlow(wtx, otherSide))
|
||||
fun validateTransaction(wtx: WireTransaction) {
|
||||
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))
|
||||
}
|
||||
|
@ -753,4 +753,5 @@ Cuautitlan Izcalli -99.25 19.65
|
||||
Nuremberg 11.05 49.45
|
||||
Santa Fe -60.69 -31.6
|
||||
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
|
||||
)
|
||||
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() }
|
||||
|
||||
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 anonymousParty = AnonymousParty(key)
|
||||
val party = Party("test key", key)
|
||||
assertEquals(party, anonymousParty)
|
||||
assertEquals(anonymousParty, party)
|
||||
assertNotEquals(AnonymousParty(differentKey), anonymousParty)
|
||||
assertNotEquals(AnonymousParty(differentKey), party)
|
||||
assertEquals<AbstractParty>(party, anonymousParty)
|
||||
assertEquals<AbstractParty>(anonymousParty, party)
|
||||
assertNotEquals<AbstractParty>(AnonymousParty(differentKey), anonymousParty)
|
||||
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
|
||||
|
||||
import com.esotericsoftware.kryo.io.Input
|
||||
import com.pholser.junit.quickcheck.From
|
||||
import com.pholser.junit.quickcheck.Property
|
||||
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.runner.JUnitQuickcheck
|
||||
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.flows.BroadcastTransactionFlow.NotifyTxRequest
|
||||
import org.junit.runner.RunWith
|
||||
@ -25,9 +24,8 @@ class BroadcastTransactionFlowTest {
|
||||
|
||||
@Property
|
||||
fun serialiseDeserialiseOfNotifyMessageWorks(@From(NotifyTxRequestMessageGenerator::class) message: NotifyTxRequest) {
|
||||
val kryo = createKryo()
|
||||
val serialized = message.serialize().bytes
|
||||
val deserialized = kryo.readClassAndObject(Input(serialized))
|
||||
val deserialized = serialized.deserialize<NotifyTxRequest>()
|
||||
assertEquals(deserialized, message)
|
||||
}
|
||||
}
|
||||
|
@ -6,11 +6,17 @@ import net.corda.core.crypto.CompositeKey
|
||||
import net.corda.core.crypto.Party
|
||||
import net.corda.core.crypto.SecureHash
|
||||
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.utilities.Emoji
|
||||
import net.corda.flows.CashIssueFlow
|
||||
import net.corda.flows.ContractUpgradeFlow
|
||||
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.testing.node.MockNetwork
|
||||
import org.junit.After
|
||||
@ -60,19 +66,82 @@ class ContractUpgradeFlowTest {
|
||||
requireNotNull(btx)
|
||||
|
||||
// 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()
|
||||
assertFailsWith(ExecutionException::class) { rejectedFuture.get() }
|
||||
|
||||
// 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.
|
||||
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()
|
||||
|
||||
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 {
|
||||
val stx = databaseTransaction(a.database) { a.services.storageService.validatedTransactions.getTransaction(result.ref.txhash) }
|
||||
requireNotNull(stx)
|
||||
@ -94,17 +163,17 @@ class ContractUpgradeFlowTest {
|
||||
mockNet.runNetwork()
|
||||
val stateAndRef = result.getOrThrow().tx.outRef<Cash.State>(0)
|
||||
// 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()
|
||||
// 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.")
|
||||
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.")
|
||||
}
|
||||
|
||||
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> {
|
||||
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