Merge remote-tracking branch 'open/master' into aslemmer-enterprise-merge-july-13

This commit is contained in:
Andras Slemmer 2017-07-13 12:35:20 +01:00
commit eac7db295c
442 changed files with 8940 additions and 6132 deletions

2
.gitignore vendored
View File

@ -88,6 +88,8 @@ docs/virtualenv/
# vim # vim
*.swp *.swp
*.swn
*.swo
# Files you may find useful to have in your working directory. # Files you may find useful to have in your working directory.
PLAN PLAN

2
.idea/compiler.xml generated
View File

@ -91,6 +91,8 @@
<module name="simm-valuation-demo_test" target="1.8" /> <module name="simm-valuation-demo_test" target="1.8" />
<module name="smoke-test-utils_main" target="1.8" /> <module name="smoke-test-utils_main" target="1.8" />
<module name="smoke-test-utils_test" target="1.8" /> <module name="smoke-test-utils_test" target="1.8" />
<module name="test-common_main" target="1.8" />
<module name="test-common_test" target="1.8" />
<module name="test-utils_integrationTest" target="1.8" /> <module name="test-utils_integrationTest" target="1.8" />
<module name="test-utils_main" target="1.8" /> <module name="test-utils_main" target="1.8" />
<module name="test-utils_test" target="1.8" /> <module name="test-utils_test" target="1.8" />

View File

@ -1,4 +1,4 @@
Corda and the Corda logo are trademarks of R3CEV LLC and its affiliates. Corda and the Corda logo are trademarks of R3 HoldCo LLC and its affiliates.
All rights reserved. All rights reserved.
For R3CEV LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-usage-policy For R3 HoldCo LLC's trademark and logo usage information, please consult our Trademark Usage Policy available at https://www.r3.com/trademark-policy

View File

@ -1,11 +1,10 @@
buildscript { buildscript {
// For sharing constants between builds // For sharing constants between builds
Properties constants = new Properties() Properties constants = new Properties()
file("$projectDir/constants.properties").withInputStream { constants.load(it) } file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// Our version: bump this on release. // Our version: bump this on release.
ext.corda_release_version = "0.13-SNAPSHOT" ext.corda_release_version = "0.14-SNAPSHOT"
// Increment this on any release that changes public APIs anywhere in the Corda platform // Increment this on any release that changes public APIs anywhere in the Corda platform
// TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal // TODO This is going to be difficult until we have a clear separation throughout the code of what is public and what is internal
ext.corda_platform_version = 1 ext.corda_platform_version = 1
@ -75,6 +74,7 @@ plugins {
// but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle. // but the DSL has some restrictions e.g can't be used on the allprojects section. So we should revisit this if there are improvements in Gradle.
// Version 1.0.2 of this plugin uses capsule:1.0.1 // Version 1.0.2 of this plugin uses capsule:1.0.1
id "us.kirchmeier.capsule" version "1.0.2" id "us.kirchmeier.capsule" version "1.0.2"
id "com.jfrog.artifactory" version "4.4.18"
} }
ext { ext {
@ -85,6 +85,7 @@ apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions' apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'net.corda.plugins.cordformation' apply plugin: 'net.corda.plugins.cordformation'
apply plugin: 'maven-publish'
// We need the following three lines even though they're inside an allprojects {} block below because otherwise // We need the following three lines even though they're inside an allprojects {} block below because otherwise
// IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along // IntelliJ gets confused when importing the project and ends up erasing and recreating the .idea directory, along
@ -103,12 +104,6 @@ allprojects {
sourceCompatibility = 1.8 sourceCompatibility = 1.8
targetCompatibility = 1.8 targetCompatibility = 1.8
// Use manual resource copying of log4j2.xml rather than source sets.
// This prevents problems in IntelliJ with regard to duplicate source roots.
processTestResources {
from file("$rootDir/config/test/log4j2.xml")
}
tasks.withType(JavaCompile) { tasks.withType(JavaCompile) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters" options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters"
} }
@ -254,7 +249,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda' projectUrl = 'https://github.com/corda/corda'
gpgSign = true gpgSign = true
gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE') gpgPassphrase = System.getenv('CORDA_BINTRAY_GPG_PASSPHRASE')
publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver'] publications = ['corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-common', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver']
license { license {
name = 'Apache-2.0' name = 'Apache-2.0'
url = 'https://www.apache.org/licenses/LICENSE-2.0' url = 'https://www.apache.org/licenses/LICENSE-2.0'
@ -279,3 +274,17 @@ task buildCordappDependenciesZip(type: Zip) {
from 'node/capsule/NOTICE' // CDDL notice from 'node/capsule/NOTICE' // CDDL notice
duplicatesStrategy = DuplicatesStrategy.EXCLUDE duplicatesStrategy = DuplicatesStrategy.EXCLUDE
} }
artifactory {
publish {
contextUrl = 'https://ci-artifactory.corda.r3cev.com/artifactory'
repository {
repoKey = 'corda-releases'
username = 'teamcity'
password = System.getenv('CORDA_ARTIFACTORY_PASSWORD')
}
defaults {
publications('corda-jfx', 'corda-mock', 'corda-rpc', 'corda-core', 'corda', 'cordform-common', 'corda-finance', 'corda-node', 'corda-node-api', 'corda-node-schemas', 'corda-test-utils', 'corda-jackson', 'corda-verifier', 'corda-webserver-impl', 'corda-webserver')
}
}
}

View File

@ -1,6 +1,7 @@
apply plugin: 'java' apply plugin: 'java'
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
dependencies { dependencies {
compile project(':core') compile project(':core')

View File

@ -10,13 +10,14 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.IdentityService import net.corda.core.node.services.IdentityService
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey

View File

@ -1,6 +1,7 @@
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Corda client JavaFX modules' description 'Corda client JavaFX modules'

View File

@ -12,19 +12,19 @@ import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE import net.corda.testing.ALICE
import net.corda.core.utilities.BOB import net.corda.testing.BOB
import net.corda.core.utilities.CHARLIE import net.corda.testing.CHARLIE
import net.corda.core.utilities.DUMMY_NOTARY import net.corda.testing.DUMMY_NOTARY
import net.corda.flows.CashExitFlow import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
@ -113,11 +113,13 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue works end to end`() { fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow, rpc.startFlow(::CashIssueFlow,
Amount(100, USD), Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })), OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity, aliceNode.legalIdentity,
notaryNode.notaryIdentity notaryNode.notaryIdentity,
anonymous
) )
vaultUpdates.expectEvents(isStrict = false) { vaultUpdates.expectEvents(isStrict = false) {
@ -138,8 +140,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test @Test
fun `cash issue and move`() { fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow() val anonymous = false
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow() rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity, anonymous).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity, anonymous).returnValue.getOrThrow()
var issueSmId: StateMachineRunId? = null var issueSmId: StateMachineRunId? = null
var moveSmId: StateMachineRunId? = null var moveSmId: StateMachineRunId? = null

View File

@ -1,18 +1,18 @@
package net.corda.client.jfx.model package net.corda.client.jfx.model
import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.node.services.NetworkMapCache.MapChange import net.corda.core.node.services.NetworkMapCache.MapChange
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
@ -51,7 +51,7 @@ class NodeMonitorModel {
* Register for updates to/from a given vault. * Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism * TODO provide an unsubscribe mechanism
*/ */
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) { fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) {
val client = CordaRPCClient( val client = CordaRPCClient(
hostAndPort = nodeHostAndPort, hostAndPort = nodeHostAndPort,
configuration = CordaRPCClientConfiguration.default.copy( configuration = CordaRPCClientConfiguration.default.copy(

View File

@ -7,17 +7,17 @@ import javafx.collections.FXCollections
import net.corda.client.jfx.utils.fold import net.corda.client.jfx.utils.fold
import net.corda.client.jfx.utils.map import net.corda.client.jfx.utils.map
import net.corda.client.jfx.utils.recordAsAssociation import net.corda.client.jfx.utils.recordAsAssociation
import net.corda.core.ErrorOr
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineUpdate import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.utilities.Try
import org.fxmisc.easybind.EasyBind import org.fxmisc.easybind.EasyBind
data class ProgressStatus(val status: String?) data class ProgressStatus(val status: String?)
sealed class StateMachineStatus { sealed class StateMachineStatus {
data class Added(val id: StateMachineRunId, val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus() data class Added(val id: StateMachineRunId, val stateMachineName: String, val flowInitiator: FlowInitiator) : StateMachineStatus()
data class Removed(val id: StateMachineRunId, val result: ErrorOr<*>) : StateMachineStatus() data class Removed(val id: StateMachineRunId, val result: Try<*>) : StateMachineStatus()
} }
data class StateMachineData( data class StateMachineData(
@ -33,11 +33,11 @@ data class Counter(
var progress: SimpleIntegerProperty = SimpleIntegerProperty(0) var progress: SimpleIntegerProperty = SimpleIntegerProperty(0)
) { ) {
fun addSmm() { progress.value += 1 } fun addSmm() { progress.value += 1 }
fun removeSmm(result: ErrorOr<*>) { fun removeSmm(result: Try<*>) {
progress.value -= 1 progress.value -= 1
when (result.error) { when (result) {
null -> success.value += 1 is Try.Success -> success.value += 1
else -> errored.value += 1 is Try.Failure -> errored.value += 1
} }
} }
} }

View File

@ -1,6 +1,7 @@
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Corda client mock modules' description 'Corda client mock modules'

View File

@ -4,7 +4,7 @@ import net.corda.core.contracts.Amount
import net.corda.core.contracts.GBP import net.corda.core.contracts.GBP
import net.corda.core.contracts.USD import net.corda.core.contracts.USD
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashFlowCommand import net.corda.flows.CashFlowCommand
import java.util.* import java.util.*
@ -26,7 +26,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy -> protected val issueCashGenerator = amountGenerator.combine(partyGenerator, issueRefGenerator, currencyGenerator) { amount, to, issueRef, ccy ->
addToMap(ccy, amount) addToMap(ccy, amount)
CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary) CashFlowCommand.IssueCash(Amount(amount, ccy), issueRef, to, notary, anonymous = true)
} }
protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy -> protected val exitCashGenerator = amountGenerator.combine(issueRefGenerator, currencyGenerator) { amount, issueRef, ccy ->
@ -35,7 +35,7 @@ open class EventGenerator(val parties: List<Party>, val currencies: List<Currenc
} }
open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> open val moveCashGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
} }
open val issuerGenerator = Generator.frequency(listOf( open val issuerGenerator = Generator.frequency(listOf(
@ -71,11 +71,11 @@ class ErrorFlowsEventGenerator(parties: List<Party>, currencies: List<Currency>,
} }
val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency -> val normalMoveGenerator = amountGenerator.combine(partyGenerator, currencyGenerator) { amountIssued, recipient, currency ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient) CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
} }
val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency -> val errorMoveGenerator = partyGenerator.combine(currencyGenerator) { recipient, currency ->
CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient) CashFlowCommand.PayCash(Amount(currencyMap[currency]!! * 2, currency), recipient, anonymous = true)
} }
override val moveCashGenerator = Generator.frequency(listOf( override val moveCashGenerator = Generator.frequency(listOf(

View File

@ -1,7 +1,7 @@
package net.corda.client.mock package net.corda.client.mock
import net.corda.client.mock.Generator.Companion.choice import net.corda.client.mock.Generator.Companion.choice
import net.corda.core.ErrorOr import net.corda.core.utilities.Try
import java.util.* import java.util.*
/** /**
@ -12,7 +12,7 @@ import java.util.*
* [Generator.choice] picks a generator from the specified list and runs that. * [Generator.choice] picks a generator from the specified list and runs that.
* [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking). * [Generator.frequency] is similar to [choice] but the probability may be specified for each generator (it is normalised before picking).
* [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities. * [Generator.combine] combines two generators of A and B with a function (A, B) -> C. Variants exist for other arities.
* [Generator.bind] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this * [Generator.flatMap] sequences two generators using an arbitrary A->Generator<B> function. Keep the usage of this
* function minimal as it may explode the stack, especially when using recursion. * function minimal as it may explode the stack, especially when using recursion.
* *
* There are other utilities as well, the type of which are usually descriptive. * There are other utilities as well, the type of which are usually descriptive.
@ -31,7 +31,7 @@ import java.util.*
* *
* The above will generate a random list of animals. * The above will generate a random list of animals.
*/ */
class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) { class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
// Functor // Functor
fun <B> map(function: (A) -> B): Generator<B> = fun <B> map(function: (A) -> B): Generator<B> =
@ -54,18 +54,19 @@ class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } })))))) product<R>(other1.product(other2.product(other3.product(other4.product(pure({ e -> { d -> { c -> { b -> { a -> function(a, b, c, d, e) } } } } }))))))
// Monad // Monad
fun <B> bind(function: (A) -> Generator<B>) = fun <B> flatMap(function: (A) -> Generator<B>): Generator<B> {
Generator { generate(it).bind { a -> function(a).generate(it) } } return Generator { random -> generate(random).flatMap { function(it).generate(random) } }
}
companion object { companion object {
fun <A> pure(value: A) = Generator { ErrorOr(value) } fun <A> pure(value: A) = Generator { Try.Success(value) }
fun <A> impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) } fun <A> impure(valueClosure: () -> A) = Generator { Try.Success(valueClosure()) }
fun <A> fail(error: Exception) = Generator<A> { ErrorOr.of(error) } fun <A> fail(error: Exception) = Generator<A> { Try.Failure(error) }
// Alternative // Alternative
fun <A> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).bind { generators[it] } fun <A> choice(generators: List<Generator<A>>) = intRange(0, generators.size - 1).flatMap { generators[it] }
fun <A> success(generate: (SplittableRandom) -> A) = Generator { ErrorOr(generate(it)) } fun <A> success(generate: (SplittableRandom) -> A) = Generator { Try.Success(generate(it)) }
fun <A> frequency(generators: List<Pair<Double, Generator<A>>>): Generator<A> { fun <A> frequency(generators: List<Pair<Double, Generator<A>>>): Generator<A> {
val ranges = mutableListOf<Pair<Double, Double>>() val ranges = mutableListOf<Pair<Double, Double>>()
var current = 0.0 var current = 0.0
@ -74,11 +75,11 @@ class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
ranges.add(Pair(current, next)) ranges.add(Pair(current, next))
current = next current = next
} }
return doubleRange(0.0, current).bind { value -> return doubleRange(0.0, current).flatMap { value ->
generators[ranges.binarySearch { range -> generators[ranges.binarySearch { (first, second) ->
if (value < range.first) { if (value < first) {
1 1
} else if (value < range.second) { } else if (value < second) {
0 0
} else { } else {
-1 -1
@ -91,14 +92,12 @@ class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
val result = mutableListOf<A>() val result = mutableListOf<A>()
for (generator in generators) { for (generator in generators) {
val element = generator.generate(it) val element = generator.generate(it)
val v = element.value when (element) {
if (v != null) { is Try.Success -> result.add(element.value)
result.add(v) is Try.Failure -> return@Generator element
} else {
return@Generator ErrorOr.of(element.error!!)
} }
} }
ErrorOr(result) Try.Success(result)
} }
} }
} }
@ -109,11 +108,9 @@ fun <A> Generator<A>.generateOrFail(random: SplittableRandom, numberOfTries: Int
var error: Throwable? = null var error: Throwable? = null
for (i in 0..numberOfTries - 1) { for (i in 0..numberOfTries - 1) {
val result = generate(random) val result = generate(random)
val v = result.value error = when (result) {
if (v != null) { is Try.Success -> return result.value
return v is Try.Failure -> result.exception
} else {
error = result.error
} }
} }
if (error == null) { if (error == null) {
@ -147,9 +144,9 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double>
fun Generator.Companion.char() = Generator { fun Generator.Companion.char() = Generator {
val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16)) val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16))
if (Character.isValidCodePoint(codePoint)) { if (Character.isValidCodePoint(codePoint)) {
return@Generator ErrorOr(codePoint.toChar()) return@Generator Try.Success(codePoint.toChar())
} else { } else {
ErrorOr.of(IllegalStateException("Could not generate valid codepoint")) Try.Failure(IllegalStateException("Could not generate valid codepoint"))
} }
} }
@ -175,20 +172,19 @@ fun <A> Generator.Companion.replicatePoisson(meanSize: Double, generator: Genera
val result = mutableListOf<A>() val result = mutableListOf<A>()
var finish = false var finish = false
while (!finish) { while (!finish) {
val errorOr = Generator.doubleRange(0.0, 1.0).generate(it).bind { value -> val result = Generator.doubleRange(0.0, 1.0).generate(it).flatMap { value ->
if (value < chance) { if (value < chance) {
generator.generate(it).map { result.add(it) } generator.generate(it).map { result.add(it) }
} else { } else {
finish = true finish = true
ErrorOr(Unit) Try.Success(Unit)
} }
} }
val e = errorOr.error if (result is Try.Failure) {
if (e != null) { return@Generator result
return@Generator ErrorOr.of(e)
} }
} }
ErrorOr(result) Try.Success(result)
} }
fun <A> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] } fun <A> Generator.Companion.pickOne(list: List<A>) = Generator.intRange(0, list.size - 1).map { list[it] }
@ -211,7 +207,7 @@ fun <A> Generator.Companion.pickN(number: Int, list: List<A>) = Generator<List<A
resultList.add(a) resultList.add(a)
} }
} }
ErrorOr(resultList) Try.Success(resultList)
} }
fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) = fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) =

View File

@ -1,7 +1,7 @@
package net.corda.client.mock package net.corda.client.mock
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import java.util.* import java.util.*
fun generateCurrency(): Generator<Currency> { fun generateCurrency(): Generator<Currency> {

View File

@ -1,6 +1,7 @@
apply plugin: 'kotlin' apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.quasar-utils' apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils' apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
description 'Corda client RPC modules' description 'Corda client RPC modules'
@ -36,10 +37,7 @@ sourceSets {
} }
processSmokeTestResources { processSmokeTestResources {
from(file("$rootDir/config/test/log4j2.xml")) { from(project(':node:capsule').tasks['buildCordaJAR']) {
rename 'log4j2\\.xml', 'log4j2-test.xml'
}
from(project(':node:capsule').tasks.buildCordaJAR) {
rename 'corda-(.*)', 'corda.jar' rename 'corda-(.*)', 'corda.jar'
} }
} }
@ -85,4 +83,4 @@ jar {
publish { publish {
name = jar.baseName name = jar.baseName
} }

View File

@ -5,9 +5,9 @@ import net.corda.core.flows.FlowInitiator
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.utilities.ALICE import net.corda.testing.ALICE
import net.corda.flows.CashException import net.corda.flows.CashException
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow

View File

@ -5,17 +5,22 @@ import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.pool.KryoPool import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.* import net.corda.core.crypto.random63BitValue
import net.corda.core.future
import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.testing.driver.poll import net.corda.core.millis
import net.corda.core.seconds
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.Try
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.nodeapi.RPCApi import net.corda.nodeapi.RPCApi
import net.corda.nodeapi.RPCKryo import net.corda.nodeapi.RPCKryo
import net.corda.testing.* import net.corda.testing.*
import net.corda.testing.driver.poll
import org.apache.activemq.artemis.api.core.SimpleString import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
@ -24,7 +29,10 @@ import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject import rx.subjects.UnicastSubject
import java.time.Duration import java.time.Duration
import java.util.concurrent.* import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class RPCStabilityTests { class RPCStabilityTests {
@ -77,9 +85,9 @@ class RPCStabilityTests {
val executor = Executors.newScheduledThreadPool(1) val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() { fun startAndStop() {
rpcDriver { rpcDriver {
ErrorOr.catch { startRpcClient<RPCOps>(HostAndPort.fromString("localhost:9999")).get() } Try.on { startRpcClient<RPCOps>(NetworkHostAndPort("localhost", 9999)).get() }
val server = startRpcServer<RPCOps>(ops = DummyOps) val server = startRpcServer<RPCOps>(ops = DummyOps)
ErrorOr.catch { startRpcClient<RPCOps>( Try.on { startRpcClient<RPCOps>(
server.get().broker.hostAndPort!!, server.get().broker.hostAndPort!!,
configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1) configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1)
).get() } ).get() }

View File

@ -1,9 +1,9 @@
package net.corda.client.rpc package net.corda.client.rpc
import com.google.common.net.HostAndPort
import net.corda.client.rpc.internal.RPCClient import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.messaging.CordaRPCOps import net.corda.core.messaging.CordaRPCOps
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
import net.corda.nodeapi.config.SSLConfiguration import net.corda.nodeapi.config.SSLConfiguration
@ -33,7 +33,7 @@ data class CordaRPCClientConfiguration(
/** @see RPCClient */ /** @see RPCClient */
class CordaRPCClient( class CordaRPCClient(
hostAndPort: HostAndPort, hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration? = null, sslConfiguration: SSLConfiguration? = null,
configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default configuration: CordaRPCClientConfiguration = CordaRPCClientConfiguration.default
) { ) {

View File

@ -1,11 +1,11 @@
package net.corda.client.rpc.internal package net.corda.client.rpc.internal
import com.google.common.net.HostAndPort
import net.corda.core.logElapsedTime import net.corda.core.logElapsedTime
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.minutes import net.corda.core.minutes
import net.corda.core.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection import net.corda.nodeapi.ConnectionDirection
@ -88,7 +88,7 @@ class RPCClient<I : RPCOps>(
val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default
) { ) {
constructor( constructor(
hostAndPort: HostAndPort, hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration? = null, sslConfiguration: SSLConfiguration? = null,
configuration: RPCClientConfiguration = RPCClientConfiguration.default configuration: RPCClientConfiguration = RPCClientConfiguration.default
) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration) ) : this(tcpTransport(ConnectionDirection.Outbound(), hostAndPort, sslConfiguration), configuration)

View File

@ -12,9 +12,9 @@ import com.google.common.cache.RemovalListener
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import com.google.common.util.concurrent.ThreadFactoryBuilder import com.google.common.util.concurrent.ThreadFactoryBuilder
import net.corda.core.ThreadBox import net.corda.core.ThreadBox
import net.corda.core.crypto.random63BitValue
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.random63BitValue
import net.corda.core.serialization.KryoPoolWithContext import net.corda.core.serialization.KryoPoolWithContext
import net.corda.core.utilities.* import net.corda.core.utilities.*
import net.corda.nodeapi.* import net.corda.nodeapi.*
@ -229,14 +229,15 @@ class RPCClientProxyHandler(
if (replyFuture == null) { if (replyFuture == null) {
log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.") log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.")
} else { } else {
val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong) val result = serverToClient.result
serverToClient.result.match( when (result) {
onError = { is Try.Success -> replyFuture.set(result.value)
if (rpcCallSite != null) addRpcCallSiteToThrowable(it, rpcCallSite) is Try.Failure -> {
replyFuture.setException(it) val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
}, if (rpcCallSite != null) addRpcCallSiteToThrowable(result.exception, rpcCallSite)
onValue = { replyFuture.set(it) } replyFuture.setException(result.exception)
) }
}
} }
} }
is RPCApi.ServerToClient.Observation -> { is RPCApi.ServerToClient.Observation -> {

View File

@ -10,17 +10,13 @@ import net.corda.core.contracts.POUNDS
import net.corda.core.contracts.SWISS_FRANCS import net.corda.core.contracts.SWISS_FRANCS
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.identity.Party
import net.corda.core.messaging.* import net.corda.core.messaging.*
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.*
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.SortAttribute
import net.corda.core.seconds import net.corda.core.seconds
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.sizedInputStreamAndHash import net.corda.core.sizedInputStreamAndHash
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor import net.corda.core.utilities.loggerFor
import net.corda.flows.CashIssueFlow import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow import net.corda.flows.CashPaymentFlow
@ -28,6 +24,7 @@ import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess import net.corda.smoketesting.NodeProcess
import org.apache.commons.io.output.NullOutputStream import org.apache.commons.io.output.NullOutputStream
import org.bouncycastle.asn1.x500.X500Name
import org.junit.After import org.junit.After
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -51,10 +48,10 @@ class StandaloneCordaRPClientTest {
private lateinit var notary: NodeProcess private lateinit var notary: NodeProcess
private lateinit var rpcProxy: CordaRPCOps private lateinit var rpcProxy: CordaRPCOps
private lateinit var connection: CordaRPCConnection private lateinit var connection: CordaRPCConnection
private lateinit var notaryIdentity: Party private lateinit var notaryNode: NodeInfo
private val notaryConfig = NodeConfig( private val notaryConfig = NodeConfig(
party = DUMMY_NOTARY, legalName = X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"),
p2pPort = port.andIncrement, p2pPort = port.andIncrement,
rpcPort = port.andIncrement, rpcPort = port.andIncrement,
webPort = port.andIncrement, webPort = port.andIncrement,
@ -67,7 +64,7 @@ class StandaloneCordaRPClientTest {
notary = NodeProcess.Factory().create(notaryConfig) notary = NodeProcess.Factory().create(notaryConfig)
connection = notary.connect() connection = notary.connect()
rpcProxy = connection.proxy rpcProxy = connection.proxy
notaryIdentity = fetchNotaryIdentity() notaryNode = fetchNotaryIdentity()
} }
@After @After
@ -95,7 +92,7 @@ class StandaloneCordaRPClientTest {
@Test @Test
fun `test starting flow`() { fun `test starting flow`() {
rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 127.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout) .returnValue.getOrThrow(timeout)
} }
@ -103,7 +100,7 @@ class StandaloneCordaRPClientTest {
fun `test starting tracked flow`() { fun `test starting tracked flow`() {
var trackCount = 0 var trackCount = 0
val handle = rpcProxy.startTrackedFlow( val handle = rpcProxy.startTrackedFlow(
::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity ::CashIssueFlow, 429.DOLLARS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity
) )
handle.progress.subscribe { msg -> handle.progress.subscribe { msg ->
log.info("Flow>> $msg") log.info("Flow>> $msg")
@ -115,7 +112,7 @@ class StandaloneCordaRPClientTest {
@Test @Test
fun `test network map`() { fun `test network map`() {
assertEquals(DUMMY_NOTARY.name, notaryIdentity.name) assertEquals(notaryConfig.legalName, notaryNode.legalIdentity.name)
} }
@Test @Test
@ -132,38 +129,15 @@ class StandaloneCordaRPClientTest {
} }
// Now issue some cash // Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 513.SWISS_FRANCS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout) .returnValue.getOrThrow(timeout)
assertEquals(1, updateCount) assertEquals(1, updateCount)
} }
@Test
fun `test vault`() {
val (vault, vaultUpdates) = rpcProxy.vaultAndUpdates()
assertEquals(0, vault.size)
var updateCount = 0
vaultUpdates.subscribe { update ->
log.info("Vault>> FlowId=${update.flowId}")
++updateCount
}
// Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity)
.returnValue.getOrThrow(timeout)
assertNotEquals(0, updateCount)
// Check that this cash exists in the vault
val cashBalance = rpcProxy.getCashBalances()
log.info("Cash Balances: $cashBalance")
assertEquals(1, cashBalance.size)
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
}
@Test @Test
fun `test vault track by`() { fun `test vault track by`() {
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>() val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>()
assertEquals(0, vault.totalStatesAvailable) assertEquals(0, vault.states.size)
var updateCount = 0 var updateCount = 0
vaultUpdates.subscribe { update -> vaultUpdates.subscribe { update ->
@ -172,7 +146,7 @@ class StandaloneCordaRPClientTest {
} }
// Now issue some cash // Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout) .returnValue.getOrThrow(timeout)
assertNotEquals(0, updateCount) assertNotEquals(0, updateCount)
@ -186,18 +160,18 @@ class StandaloneCordaRPClientTest {
@Test @Test
fun `test vault query by`() { fun `test vault query by`() {
// Now issue some cash // Now issue some cash
rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryIdentity, notaryIdentity) rpcProxy.startFlow(::CashIssueFlow, 629.POUNDS, OpaqueBytes.of(0), notaryNode.legalIdentity, notaryNode.notaryIdentity)
.returnValue.getOrThrow(timeout) .returnValue.getOrThrow(timeout)
val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL) val criteria = QueryCriteria.VaultQueryCriteria(status = Vault.StateStatus.ALL)
val paging = PageSpecification(0, 10) val paging = PageSpecification(DEFAULT_PAGE_NUM, 10)
val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC))) val sorting = Sort(setOf(Sort.SortColumn(SortAttribute.Standard(Sort.VaultStateAttribute.RECORDED_TIME), Sort.Direction.DESC)))
val queryResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting) val queryResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
assertEquals(1, queryResults.totalStatesAvailable) assertEquals(1, queryResults.totalStatesAvailable)
assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity) assertEquals(queryResults.states.first().state.data.amount.quantity, 629.POUNDS.quantity)
rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryIdentity).returnValue.getOrThrow() rpcProxy.startFlow(::CashPaymentFlow, 100.POUNDS, notaryNode.legalIdentity).returnValue.getOrThrow()
val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting) val moreResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100 assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
@ -209,11 +183,11 @@ class StandaloneCordaRPClientTest {
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")]) assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
} }
private fun fetchNotaryIdentity(): Party { private fun fetchNotaryIdentity(): NodeInfo {
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates() val (nodeInfo, nodeUpdates) = rpcProxy.networkMapFeed()
nodeUpdates.notUsed() nodeUpdates.notUsed()
assertEquals(1, nodeInfo.size) assertEquals(1, nodeInfo.size)
return nodeInfo[0].legalIdentity return nodeInfo[0]
} }
// This InputStream cannot have been whitelisted. // This InputStream cannot have been whitelisted.

View File

@ -0,0 +1,27 @@
package net.corda.kotlin.rpc
import net.corda.core.div
import org.junit.Test
import java.io.File
import java.nio.file.Path
import java.nio.file.Paths
import kotlin.test.assertFalse
import kotlin.test.assertTrue
class ValidateClasspathTest {
@Test
fun `node not on classpath`() {
val paths = System.getProperty("java.class.path").split(File.pathSeparatorChar).map { Paths.get(it) }
// First find core so that if node is there, it's in the form we expect:
assertFalse(paths.filter { it.contains("core" / "build") }.isEmpty())
assertTrue(paths.filter { it.contains("node" / "build") }.isEmpty())
}
}
private fun Path.contains(that: Path): Boolean {
val size = that.nameCount
(0..nameCount - size).forEach {
if (subpath(it, it + size) == that) return true
}
return false
}

View File

@ -5,7 +5,7 @@ import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import net.corda.core.getOrThrow import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.success import net.corda.core.thenMatch
import net.corda.node.services.messaging.getRpcContext import net.corda.node.services.messaging.getRpcContext
import net.corda.nodeapi.RPCSinceVersion import net.corda.nodeapi.RPCSinceVersion
import net.corda.testing.RPCDriverExposedDSLInterface import net.corda.testing.RPCDriverExposedDSLInterface
@ -158,12 +158,12 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
val clientQuotes = LinkedBlockingQueue<String>() val clientQuotes = LinkedBlockingQueue<String>()
val clientFuture = proxy.makeComplicatedListenableFuture() val clientFuture = proxy.makeComplicatedListenableFuture()
clientFuture.success { clientFuture.thenMatch({
val name = it.first val name = it.first
it.second.success { it.second.thenMatch({
clientQuotes += "Quote by $name: $it" clientQuotes += "Quote by $name: $it"
} }, {})
} }, {})
assertThat(clientQuotes).isEmpty() assertThat(clientQuotes).isEmpty()

View File

@ -4,7 +4,7 @@ import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.future import net.corda.core.future
import net.corda.core.messaging.RPCOps import net.corda.core.messaging.RPCOps
import net.corda.core.millis import net.corda.core.millis
import net.corda.core.random63BitValue import net.corda.core.crypto.random63BitValue
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.node.services.messaging.RPCServerConfiguration import net.corda.node.services.messaging.RPCServerConfiguration
import net.corda.testing.RPCDriverExposedDSLInterface import net.corda.testing.RPCDriverExposedDSLInterface

View File

@ -13,7 +13,7 @@
<Appenders> <Appenders>
<Console name="Console-Appender" target="SYSTEM_OUT"> <Console name="Console-Appender" target="SYSTEM_OUT">
<PatternLayout pattern="%highlight{%level{length=1} %date{HH:mm:ss} [%t] %c{2}.%method - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" /> <PatternLayout pattern="%highlight{%level{length=1} %date{HH:mm:ssZ} [%t] %c{2}.%method - %msg%n}{INFO=white,WARN=red,FATAL=bright red}" />
</Console> </Console>
<!-- Required for printBasicInfo --> <!-- Required for printBasicInfo -->
@ -27,7 +27,7 @@
fileName="${sys:log-path}/${log-name}.log" fileName="${sys:log-path}/${log-name}.log"
filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz"> filePattern="${archive}/${log-name}.%date{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="[%-5level] %date{ISO8601}{GMT+0} [%t] %c{2}.%method - %msg%n"/> <PatternLayout pattern="[%-5level] %date{ISO8601}{UTC}Z [%t] %c{2}.%method - %msg%n"/>
<Policies> <Policies>
<TimeBasedTriggeringPolicy/> <TimeBasedTriggeringPolicy/>

View File

@ -1,4 +1,4 @@
gradlePluginsVersion=0.12.4 gradlePluginsVersion=0.13.2
kotlinVersion=1.1.1 kotlinVersion=1.1.1
guavaVersion=21.0 guavaVersion=21.0
bouncycastleVersion=1.57 bouncycastleVersion=1.57

View File

@ -6,6 +6,10 @@ repositories {
mavenCentral() mavenCentral()
} }
// This tracks the gradle plugins version and not Corda
version gradle_plugins_version
group 'net.corda.plugins'
dependencies { dependencies {
// TypeSafe Config: for simple and human friendly config files. // TypeSafe Config: for simple and human friendly config files.
compile "com.typesafe:config:$typesafe_config_version" compile "com.typesafe:config:$typesafe_config_version"

View File

@ -7,7 +7,7 @@ import com.typesafe.config.ConfigValueFactory;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public class CordformNode { public class CordformNode implements NodeDefinition {
protected static final String DEFAULT_HOST = "localhost"; protected static final String DEFAULT_HOST = "localhost";
/** /**

View File

@ -0,0 +1,9 @@
package net.corda.cordform;
import com.typesafe.config.Config;
public interface NodeDefinition {
String getName();
Config getConfig();
}

View File

@ -1,4 +1,4 @@
package net.corda.core.utilities package net.corda.core
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import java.util.* import java.util.*

View File

@ -7,7 +7,6 @@ import com.google.common.base.Throwables
import com.google.common.io.ByteStreams import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.* import com.google.common.util.concurrent.*
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256 import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowException import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
@ -24,9 +23,11 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute import java.nio.file.attribute.FileAttribute
import java.time.Duration import java.time.Duration
import java.time.temporal.Temporal import java.time.temporal.Temporal
import java.util.concurrent.* import java.util.concurrent.CompletableFuture
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import java.util.function.BiConsumer
import java.util.stream.Stream import java.util.stream.Stream
import java.util.zip.Deflater import java.util.zip.Deflater
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@ -59,12 +60,6 @@ infix fun Int.checkedAdd(b: Int) = Math.addExact(this, b)
@Suppress("unused") @Suppress("unused")
infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b) infix fun Long.checkedAdd(b: Long) = Math.addExact(this, b)
/**
* Returns a random positive long generated using a secure RNG. This function sacrifies a bit of entropy in order to
* avoid potential bugs where the value is used in a context where negative numbers are not expected.
*/
fun random63BitValue(): Long = Math.abs(newSecureRandom().nextLong())
/** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */ /** Same as [Future.get] but with a more descriptive name, and doesn't throw [ExecutionException], instead throwing its cause */
fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T { fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T {
return try { return try {
@ -74,38 +69,20 @@ fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T {
} }
} }
fun <T> future(block: () -> T): ListenableFuture<T> = CompletableToListenable(CompletableFuture.supplyAsync(block)) fun <V> future(block: () -> V): Future<V> = CompletableFuture.supplyAsync(block)
private class CompletableToListenable<T>(private val base: CompletableFuture<T>) : Future<T> by base, ListenableFuture<T> { fun <F : ListenableFuture<*>, V> F.then(block: (F) -> V) = addListener(Runnable { block(this) }, MoreExecutors.directExecutor())
override fun addListener(listener: Runnable, executor: Executor) {
base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor)
}
}
// Some utilities for working with Guava listenable futures. fun <U, V> Future<U>.match(success: (U) -> V, failure: (Throwable) -> V): V {
fun <T> ListenableFuture<T>.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor) return success(try {
fun <T> ListenableFuture<T>.success(executor: Executor, body: (T) -> Unit) = then(executor) {
val r = try {
get()
} catch(e: Throwable) {
return@then
}
body(r)
}
fun <T> ListenableFuture<T>.failure(executor: Executor, body: (Throwable) -> Unit) = then(executor) {
try {
getOrThrow() getOrThrow()
} catch (t: Throwable) { } catch (t: Throwable) {
body(t) return failure(t)
} })
} }
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) } fun <U, V, W> ListenableFuture<U>.thenMatch(success: (U) -> V, failure: (Throwable) -> W) = then { it.match(success, failure) }
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) } fun ListenableFuture<*>.andForget(log: Logger) = then { it.match({}, { log.error("Background task failed:", it) }) }
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) }
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe. @Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) }) infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) } infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
@ -121,12 +98,12 @@ inline fun <T> SettableFuture<T>.catch(block: () -> T) {
fun <A> ListenableFuture<out A>.toObservable(): Observable<A> { fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
return Observable.create { subscriber -> return Observable.create { subscriber ->
success { thenMatch({
subscriber.onNext(it) subscriber.onNext(it)
subscriber.onCompleted() subscriber.onCompleted()
} failure { }, {
subscriber.onError(it) subscriber.onError(it)
} })
} }
} }
@ -211,9 +188,6 @@ fun <T> List<T>.randomOrNull(): T? {
/** Returns a random element in the list matching the given predicate, or null if none found */ /** Returns a random element in the list matching the given predicate, or null if none found */
fun <T> List<T>.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).randomOrNull() fun <T> List<T>.randomOrNull(predicate: (T) -> Boolean) = filter(predicate).randomOrNull()
// An alias that can sometimes make code clearer to read.
val RunOnCallerThread: Executor = MoreExecutors.directExecutor()
inline fun elapsedTime(block: () -> Unit): Duration { inline fun elapsedTime(block: () -> Unit): Duration {
val start = System.nanoTime() val start = System.nanoTime()
block() block()
@ -353,63 +327,6 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this) val Throwable.rootCause: Throwable get() = Throwables.getRootCause(this)
/** Representation of an operation that may have thrown an error. */
@Suppress("DataClassPrivateConstructor")
@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)
companion object {
/** Runs the given lambda and wraps the result. */
inline fun <T : Any> catch(body: () -> T): ErrorOr<T> {
return try {
ErrorOr(body())
} catch (t: Throwable) {
ErrorOr.of(t)
}
}
fun of(t: Throwable) = ErrorOr(null, t)
}
fun <T> match(onValue: (A) -> T, onError: (Throwable) -> T): T {
if (error == null) {
return onValue(value as A)
} else {
return onError(error)
}
}
fun getOrThrow(): A {
if (error == null) {
return value as A
} else {
throw error
}
}
// Functor
fun <B> map(function: (A) -> B) = ErrorOr(value?.let(function), error)
// Applicative
fun <B, C> combine(other: ErrorOr<B>, function: (A, B) -> C): ErrorOr<C> {
val newError = error ?: other.error
return ErrorOr(if (newError != null) null else function(value as A, other.value as B), newError)
}
// Monad
fun <B> bind(function: (A) -> ErrorOr<B>): ErrorOr<B> {
return if (error == null) {
function(value as A)
} else {
ErrorOr.of(error)
}
}
fun mapError(function: (Throwable) -> Throwable) = ErrorOr(value, error?.let(function))
}
/** /**
* Returns an Observable that buffers events until subscribed. * Returns an Observable that buffers events until subscribed.
* @see UnicastSubject * @see UnicastSubject

View File

@ -0,0 +1,37 @@
package net.corda.core.concurrent
import com.google.common.annotations.VisibleForTesting
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
import net.corda.core.catch
import net.corda.core.match
import net.corda.core.then
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.atomic.AtomicBoolean
/**
* As soon as a given future becomes done, the handler is invoked with that future as its argument.
* The result of the handler is copied into the result future, and the handler isn't invoked again.
* If a given future errors after the result future is done, the error is automatically logged.
*/
fun <S, T> firstOf(vararg futures: ListenableFuture<out S>, handler: (ListenableFuture<out S>) -> T) = firstOf(futures, defaultLog, handler)
private val defaultLog = LoggerFactory.getLogger("net.corda.core.concurrent")
@VisibleForTesting
internal val shortCircuitedTaskFailedMessage = "Short-circuited task failed:"
internal fun <S, T> firstOf(futures: Array<out ListenableFuture<out S>>, log: Logger, handler: (ListenableFuture<out S>) -> T): ListenableFuture<T> {
val resultFuture = SettableFuture.create<T>()
val winnerChosen = AtomicBoolean()
futures.forEach {
it.then {
if (winnerChosen.compareAndSet(false, true)) {
resultFuture.catch { handler(it) }
} else if (!it.isCancelled) {
it.match({}, { log.error(shortCircuitedTaskFailedMessage, it) })
}
}
}
return resultFuture
}

View File

@ -7,6 +7,7 @@ import net.corda.core.flows.FlowLogicRefFactory
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.* import net.corda.core.serialization.*
import net.corda.core.utilities.OpaqueBytes
import java.io.FileNotFoundException import java.io.FileNotFoundException
import java.io.IOException import java.io.IOException
import java.io.InputStream import java.io.InputStream
@ -79,8 +80,7 @@ interface ContractState {
* so that they receive the updated state, and don't end up in a situation where they can no longer use a state * so that they receive the updated state, and don't end up in a situation where they can no longer use a state
* they possess, since someone consumed that state during the notary change process. * they possess, since someone consumed that state during the notary change process.
* *
* The participants list should normally be derived from the contents of the state. E.g. for [Cash] the participants * The participants list should normally be derived from the contents of the state.
* list should just contain the owner.
*/ */
val participants: List<AbstractParty> val participants: List<AbstractParty>
} }
@ -126,7 +126,7 @@ infix fun <T : ContractState> T.withNotary(newNotary: Party) = TransactionState(
* Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's * Definition for an issued product, which can be cash, a cash-like thing, assets, or generally anything else that's
* quantifiable with integer quantities. * quantifiable with integer quantities.
* *
* @param P the type of product underlying the definition, for example [Currency]. * @param P the type of product underlying the definition, for example [java.util.Currency].
*/ */
@CordaSerializable @CordaSerializable
data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) { data class Issued<out P : Any>(val issuer: PartyAndReference, val product: P) {
@ -159,8 +159,8 @@ interface Scheduled {
} }
/** /**
* Represents a contract state (unconsumed output) of type [LinearState] and a point in time that a lifecycle event is expected to take place * Represents a contract state (unconsumed output) of type [LinearState] and a point in time that a lifecycle event is
* for that contract state. * expected to take place for that contract state.
* *
* This is effectively the input to a scheduler, which wakes up at that point in time and asks the contract state what * This is effectively the input to a scheduler, which wakes up at that point in time and asks the contract state what
* lifecycle processing needs to take place. e.g. a fixing or a late payment etc. * lifecycle processing needs to take place. e.g. a fixing or a late payment etc.
@ -168,10 +168,11 @@ interface Scheduled {
data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instant) : Scheduled data class ScheduledStateRef(val ref: StateRef, override val scheduledAt: Instant) : Scheduled
/** /**
* This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a given point in time. * This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a
* e.g. run a fixing flow. * given point in time. e.g. run a fixing flow.
* *
* Note the use of [FlowLogicRef] to represent a safe way to transport a [FlowLogic] out of the contract sandbox. * Note the use of [FlowLogicRef] to represent a safe way to transport a [net.corda.core.flows.FlowLogic] out of the
* contract sandbox.
* *
* Currently we support only flow based activities as we expect there to be a transaction generated off the back of * Currently we support only flow based activities as we expect there to be a transaction generated off the back of
* the activity, otherwise we have to start tracking secondary state on the platform of which scheduled activities * the activity, otherwise we have to start tracking secondary state on the platform of which scheduled activities
@ -383,9 +384,9 @@ class TimeWindow private constructor(
// DOCSTART 5 // DOCSTART 5
/** /**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for * Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the * every [net.corda.core.transactions.LedgerTransaction] they see on the network, for every input and output state. All
* transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted * contracts must accept the transaction for it to be accepted: failure of any aborts the entire thing. The time is taken
* time-window attached to the transaction itself i.e. it is NOT necessarily the current time. * from a trusted time-window attached to the transaction itself i.e. it is NOT necessarily the current time.
* *
* TODO: Contract serialization is likely to change, so the annotation is likely temporary. * TODO: Contract serialization is likely to change, so the annotation is likely temporary.
*/ */
@ -461,9 +462,8 @@ interface Attachment : NamedByHash {
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment { abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
companion object { companion object {
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray { fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
val storage = serviceHub.storageService.attachments
return { return {
val a = storage.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id)) val a = serviceHub.attachments.openAttachment(id) ?: throw MissingAttachmentsException(listOf(id))
(a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() } (a as? AbstractAttachment)?.attachmentData ?: a.open().use { it.readBytes() }
} }
} }

View File

@ -1,7 +1,7 @@
package net.corda.core.contracts package net.corda.core.contracts
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.node.services.ReadOnlyTransactionStorage import net.corda.core.node.services.TransactionStorage
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import java.util.* import java.util.*
@ -18,7 +18,7 @@ import java.util.concurrent.Callable
* @param transactions map of transaction id to [SignedTransaction]. * @param transactions map of transaction id to [SignedTransaction].
* @param startPoints transactions to use as starting points for the search. * @param startPoints transactions to use as starting points for the search.
*/ */
class TransactionGraphSearch(val transactions: ReadOnlyTransactionStorage, class TransactionGraphSearch(val transactions: TransactionStorage,
val startPoints: List<WireTransaction>) : Callable<List<WireTransaction>> { val startPoints: List<WireTransaction>) : Callable<List<WireTransaction>> {
class Query( class Query(
val withCommandOfType: Class<out CommandData>? = null, val withCommandOfType: Class<out CommandData>? = null,

View File

@ -2,7 +2,6 @@ package net.corda.core.contracts
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey import java.security.PublicKey
@ -61,7 +60,7 @@ sealed class TransactionType {
abstract fun verifyTransaction(tx: LedgerTransaction) abstract fun verifyTransaction(tx: LedgerTransaction)
/** A general transaction type where transaction validity is determined by custom contract code */ /** A general transaction type where transaction validity is determined by custom contract code */
object General : TransactionType(), DeserializeAsKotlinObjectDef { object General : TransactionType() {
/** Just uses the default [TransactionBuilder] with no special logic */ /** Just uses the default [TransactionBuilder] with no special logic */
class Builder(notary: Party?) : TransactionBuilder(General, notary) class Builder(notary: Party?) : TransactionBuilder(General, notary)
@ -141,15 +140,16 @@ sealed class TransactionType {
* A special transaction type for reassigning a notary for a state. Validation does not involve running * A special transaction type for reassigning a notary for a state. Validation does not involve running
* any contract code, it just checks that the states are unmodified apart from the notary field. * any contract code, it just checks that the states are unmodified apart from the notary field.
*/ */
object NotaryChange : TransactionType(), DeserializeAsKotlinObjectDef { object NotaryChange : TransactionType() {
/** /**
* A transaction builder that automatically sets the transaction type to [NotaryChange] * A transaction builder that automatically sets the transaction type to [NotaryChange]
* and adds the list of participants to the signers set for every input state. * and adds the list of participants to the signers set for every input state.
*/ */
class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) { class Builder(notary: Party) : TransactionBuilder(NotaryChange, notary) {
override fun addInputState(stateAndRef: StateAndRef<*>) { override fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
signers.addAll(stateAndRef.state.data.participants.map { it.owningKey }) signers.addAll(stateAndRef.state.data.participants.map { it.owningKey })
super.addInputState(stateAndRef) super.addInputState(stateAndRef)
return this
} }
} }

View File

@ -1,161 +0,0 @@
package net.corda.core.crypto
import net.corda.core.crypto.CompositeKey.NodeAndWeight
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.security.PublicKey
/**
* A tree data structure that enables the representation of composite public keys.
* Notice that with that implementation CompositeKey extends PublicKey. Leaves are represented by single public keys.
*
* For complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
* the requirement by creating a tree with a root [CompositeKey], and Alice and Bob as children.
* The root node would specify *weights* for each of its children and a *threshold* the minimum total weight required
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
*
* 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"*.
*
* [CompositeKey] maintains a list of [NodeAndWeight]s which holds child subtree with associated weight carried by child node signatures.
*
* The [threshold] specifies the minimum total weight required (in the simple case the minimum number of child
* signatures required) to satisfy the sub-tree rooted at this node.
*/
@CordaSerializable
class CompositeKey private constructor (val threshold: Int,
children: List<NodeAndWeight>) : PublicKey {
val children = children.sorted()
init {
require (children.size == children.toSet().size) { "Trying to construct CompositeKey with duplicated child nodes." }
// If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees but having different structures.
require(children.size > 1) { "Cannot construct CompositeKey with only one child node." }
}
/**
* Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
*/
@CordaSerializable
data class NodeAndWeight(val node: PublicKey, val weight: Int): Comparable<NodeAndWeight>, ASN1Object() {
override fun compareTo(other: NodeAndWeight): Int {
if (weight == other.weight) {
return node.hashCode().compareTo(other.node.hashCode())
}
else return weight.compareTo(other.weight)
}
override fun toASN1Primitive(): ASN1Primitive {
val vector = ASN1EncodableVector()
vector.add(DERBitString(node.encoded))
vector.add(ASN1Integer(weight.toLong()))
return DERSequence(vector)
}
}
companion object {
val ALGORITHM = CompositeSignature.ALGORITHM_IDENTIFIER.algorithm.toString()
}
/**
* Takes single PublicKey and checks if CompositeKey requirements hold for that key.
*/
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
override fun getAlgorithm() = ALGORITHM
override fun getEncoded(): ByteArray {
val keyVector = ASN1EncodableVector()
val childrenVector = ASN1EncodableVector()
children.forEach {
childrenVector.add(it.toASN1Primitive())
}
keyVector.add(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector))
return SubjectPublicKeyInfo(CompositeSignature.ALGORITHM_IDENTIFIER, DERSequence(keyVector)).encoded
}
override fun getFormat() = ASN1Encoding.DER
/**
* Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite
* key tree in question, and the total combined weight of all children is calculated for every intermediary node.
* If all thresholds are satisfied, the composite key requirement is considered to be met.
*/
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
if (keysToCheck.any { it is CompositeKey } ) return false
val totalWeight = children.map { (node, weight) ->
if (node is CompositeKey) {
if (node.isFulfilledBy(keysToCheck)) weight else 0
} else {
if (keysToCheck.contains(node)) weight else 0
}
}.sum()
return totalWeight >= threshold
}
/**
* Set of all leaf keys of that CompositeKey.
*/
val leafKeys: Set<PublicKey>
get() = children.flatMap { it.node.keys }.toSet() // Uses PublicKey.keys extension.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CompositeKey) return false
if (threshold != other.threshold) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = threshold
result = 31 * result + children.hashCode()
return result
}
override fun toString() = "(${children.joinToString()})"
/** A helper class for building a [CompositeKey]. */
class Builder {
private val children: MutableList<NodeAndWeight> = mutableListOf()
/** Adds a child [CompositeKey] node. Specifying a [weight] for the child is optional and will default to 1. */
fun addKey(key: PublicKey, weight: Int = 1): Builder {
children.add(NodeAndWeight(key, weight))
return this
}
fun addKeys(vararg keys: PublicKey): Builder {
keys.forEach { addKey(it) }
return this
}
fun addKeys(keys: List<PublicKey>): Builder = addKeys(*keys.toTypedArray())
/**
* Builds the [CompositeKey]. If [threshold] is not specified, it will default to
* the size of the children, effectively generating an "N of N" requirement.
* During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes.
*/
@Throws(IllegalArgumentException::class)
fun build(threshold: Int? = null): PublicKey {
val n = children.size
if (n > 1)
return CompositeKey(threshold ?: n, children)
else if (n == 1) {
require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
return children.first().node // We can assume that this node is a correct CompositeKey.
}
else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
}
}
}
/**
* Expands all [CompositeKey]s present in PublicKey iterable to set of single [PublicKey]s.
* If an element of the set is a single PublicKey it gives just that key, if it is a [CompositeKey] it returns all leaf
* keys for that composite element.
*/
val Iterable<PublicKey>.expandedCompositeKeys: Set<PublicKey>
get() = flatMap { it.keys }.toSet()

View File

@ -14,7 +14,7 @@ import java.security.Signature
*/ */
object ContentSignerBuilder { object ContentSignerBuilder {
fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner { fun build(signatureScheme: SignatureScheme, privateKey: PrivateKey, provider: Provider?, random: SecureRandom? = null): ContentSigner {
val sigAlgId = AlgorithmIdentifier(signatureScheme.signatureOID) val sigAlgId = signatureScheme.signatureOID
val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply { val sig = Signature.getInstance(signatureScheme.signatureName, provider).apply {
if (random != null) { if (random != null) {
initSign(privateKey, random) initSign(privateKey, random)

View File

@ -1,24 +1,26 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.random63BitValue import net.corda.core.crypto.composite.CompositeKey
import net.i2p.crypto.eddsa.* import net.corda.core.crypto.composite.CompositeSignature
import net.corda.core.crypto.provider.CordaObjectIdentifier
import net.corda.core.crypto.provider.CordaSecurityProvider
import net.i2p.crypto.eddsa.EdDSAEngine
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.EdDSASecurityProvider
import net.i2p.crypto.eddsa.math.GroupElement import net.i2p.crypto.eddsa.math.GroupElement
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1EncodableVector import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.bc.BCObjectIdentifiers import org.bouncycastle.asn1.bc.BCObjectIdentifiers
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.asn1.sec.SECObjectIdentifiers
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.*
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
import org.bouncycastle.cert.X509CertificateHolder import org.bouncycastle.cert.X509CertificateHolder
import org.bouncycastle.cert.X509v3CertificateBuilder import org.bouncycastle.cert.X509v3CertificateBuilder
@ -45,13 +47,8 @@ import org.bouncycastle.pqc.jcajce.provider.BouncyCastlePQCProvider
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec import org.bouncycastle.pqc.jcajce.spec.SPHINCS256KeyGenParameterSpec
import sun.security.pkcs.PKCS8Key
import sun.security.util.DerValue
import sun.security.x509.X509Key
import java.math.BigInteger import java.math.BigInteger
import java.security.* import java.security.*
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.spec.InvalidKeySpecException import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec import java.security.spec.X509EncodedKeySpec
@ -80,7 +77,8 @@ object Crypto {
val RSA_SHA256 = SignatureScheme( val RSA_SHA256 = SignatureScheme(
1, 1,
"RSA_SHA256", "RSA_SHA256",
PKCSObjectIdentifiers.id_RSASSA_PSS, AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null),
emptyList(),
BouncyCastleProvider.PROVIDER_NAME, BouncyCastleProvider.PROVIDER_NAME,
"RSA", "RSA",
"SHA256WITHRSAANDMGF1", "SHA256WITHRSAANDMGF1",
@ -93,7 +91,8 @@ object Crypto {
val ECDSA_SECP256K1_SHA256 = SignatureScheme( val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2, 2,
"ECDSA_SECP256K1_SHA256", "ECDSA_SECP256K1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256, AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256k1),
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256k1)),
BouncyCastleProvider.PROVIDER_NAME, BouncyCastleProvider.PROVIDER_NAME,
"ECDSA", "ECDSA",
"SHA256withECDSA", "SHA256withECDSA",
@ -106,7 +105,8 @@ object Crypto {
val ECDSA_SECP256R1_SHA256 = SignatureScheme( val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3, 3,
"ECDSA_SECP256R1_SHA256", "ECDSA_SECP256R1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256, AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256r1),
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256r1)),
BouncyCastleProvider.PROVIDER_NAME, BouncyCastleProvider.PROVIDER_NAME,
"ECDSA", "ECDSA",
"SHA256withECDSA", "SHA256withECDSA",
@ -119,10 +119,12 @@ object Crypto {
val EDDSA_ED25519_SHA512 = SignatureScheme( val EDDSA_ED25519_SHA512 = SignatureScheme(
4, 4,
"EDDSA_ED25519_SHA512", "EDDSA_ED25519_SHA512",
ASN1ObjectIdentifier("1.3.101.112"), // OID taken from https://tools.ietf.org/html/draft-ietf-curdle-pkix-00
AlgorithmIdentifier(ASN1ObjectIdentifier("1.3.101.112"), null),
emptyList(),
// We added EdDSA to bouncy castle for certificate signing. // We added EdDSA to bouncy castle for certificate signing.
BouncyCastleProvider.PROVIDER_NAME, BouncyCastleProvider.PROVIDER_NAME,
EdDSAKey.KEY_ALGORITHM, "1.3.101.112",
EdDSAEngine.SIGNATURE_ALGORITHM, EdDSAEngine.SIGNATURE_ALGORITHM,
EdDSANamedCurveTable.getByName("ED25519"), EdDSANamedCurveTable.getByName("ED25519"),
256, 256,
@ -133,10 +135,12 @@ object Crypto {
* SPHINCS-256 hash-based signature scheme. It provides 128bit security against post-quantum attackers * 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. * at the cost of larger key sizes and loss of compatibility.
*/ */
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
val SPHINCS256_SHA256 = SignatureScheme( val SPHINCS256_SHA256 = SignatureScheme(
5, 5,
"SPHINCS-256_SHA512", "SPHINCS-256_SHA512",
BCObjectIdentifiers.sphincs256_with_SHA512, AlgorithmIdentifier(BCObjectIdentifiers.sphincs256_with_SHA512, DLSequence(arrayOf(ASN1Integer(0), SHA512_256))),
listOf(AlgorithmIdentifier(BCObjectIdentifiers.sphincs256, DLSequence(arrayOf(ASN1Integer(0), SHA512_256)))),
"BCPQC", "BCPQC",
"SPHINCS256", "SPHINCS256",
"SHA512WITHSPHINCS256", "SHA512WITHSPHINCS256",
@ -146,6 +150,22 @@ object Crypto {
"at the cost of larger key sizes and loss of compatibility." "at the cost of larger key sizes and loss of compatibility."
) )
/**
* Corda composite key type
*/
val COMPOSITE_KEY = SignatureScheme(
6,
"COMPOSITE",
AlgorithmIdentifier(CordaObjectIdentifier.compositeKey),
emptyList(),
CordaSecurityProvider.PROVIDER_NAME,
CompositeKey.KEY_ALGORITHM,
CompositeSignature.SIGNATURE_ALGORITHM,
null,
null,
"Composite keys composed from individual public keys"
)
/** Our default signature scheme if no algorithm is specified (e.g. for key generation). */ /** Our default signature scheme if no algorithm is specified (e.g. for key generation). */
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512 val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
@ -158,12 +178,18 @@ object Crypto {
ECDSA_SECP256K1_SHA256, ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256, ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512, EDDSA_ED25519_SHA512,
SPHINCS256_SHA256 SPHINCS256_SHA256,
COMPOSITE_KEY
).associateBy { it.schemeCodeName } ).associateBy { it.schemeCodeName }
// We need to group signature schemes per algorithm, so to quickly identify them during decoding. /**
// Please note there are schemes with the same algorithm, e.g. EC (or ECDSA) keys are used for both ECDSA_SECP256K1_SHA256 and ECDSA_SECP256R1_SHA256. * Map of X.509 algorithm identifiers to signature schemes Corda recognises. See RFC 2459 for the format of
private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName } * algorithm identifiers.
*/
private val algorithmMap: Map<AlgorithmIdentifier, SignatureScheme>
= (supportedSignatureSchemes.values.flatMap { scheme -> scheme.alternativeOIDs.map { oid -> Pair(oid, scheme) } }
+ supportedSignatureSchemes.values.map { Pair(it.signatureOID, it) })
.toMap()
// This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider // This map is required to defend against users that forcibly call Security.addProvider / Security.removeProvider
// that could cause unexpected and suspicious behaviour. // that could cause unexpected and suspicious behaviour.
@ -171,17 +197,34 @@ object Crypto {
// The val is private to avoid any harmful state changes. // The val is private to avoid any harmful state changes.
private val providerMap: Map<String, Provider> = mapOf( private val providerMap: Map<String, Provider> = mapOf(
BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(), BouncyCastleProvider.PROVIDER_NAME to getBouncyCastleProvider(),
CordaSecurityProvider.PROVIDER_NAME to CordaSecurityProvider(),
"BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it. "BCPQC" to BouncyCastlePQCProvider()) // unfortunately, provider's name is not final in BouncyCastlePQCProvider, so we explicitly set it.
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply { private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
putAll(EdDSASecurityProvider()) putAll(EdDSASecurityProvider())
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID, KeyInfoConverter(EDDSA_ED25519_SHA512)) addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
} }
init { init {
// This registration is needed for reading back EdDSA key from java keystore. // This registration is needed for reading back EdDSA key from java keystore.
// TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider. // TODO: Find a way to make JKS work with bouncy castle provider or implement our own provide so we don't have to register bouncy castle provider.
Security.addProvider(getBouncyCastleProvider()) Security.addProvider(getBouncyCastleProvider())
Security.addProvider(CordaSecurityProvider())
}
/**
* Normalise an algorithm identifier by converting [DERNull] parameters into a Kotlin null value.
*/
private fun normaliseAlgorithmIdentifier(id: AlgorithmIdentifier): AlgorithmIdentifier {
return if (id.parameters is DERNull) {
AlgorithmIdentifier(id.algorithm, null)
} else {
id
}
}
fun findSignatureScheme(algorithm: AlgorithmIdentifier): SignatureScheme {
return algorithmMap[normaliseAlgorithmIdentifier(algorithm)] ?: throw IllegalArgumentException("Unrecognised algorithm: ${algorithm.algorithm.id}")
} }
/** /**
@ -192,6 +235,7 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested signature scheme is not supported. * @throws IllegalArgumentException if the requested signature scheme is not supported.
*/ */
@Throws(IllegalArgumentException::class)
fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName") fun findSignatureScheme(schemeCodeName: String): SignatureScheme = supportedSignatureSchemes[schemeCodeName] ?: throw IllegalArgumentException("Unsupported key/algorithm for schemeCodeName: $schemeCodeName")
/** /**
@ -202,10 +246,24 @@ object Crypto {
* @return a currently supported SignatureScheme. * @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported. * @throws IllegalArgumentException if the requested key type is not supported.
*/ */
fun findSignatureScheme(key: Key): SignatureScheme { @Throws(IllegalArgumentException::class)
val algorithm = matchingAlgorithmName(key.algorithm) fun findSignatureScheme(key: PublicKey): SignatureScheme {
algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it } val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format") return findSignatureScheme(keyInfo.algorithm)
}
/**
* 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.
* For the supported signature schemes see [Crypto].
* @param key either private or public.
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported.
*/
@Throws(IllegalArgumentException::class)
fun findSignatureScheme(key: PrivateKey): SignatureScheme {
val keyInfo = PrivateKeyInfo.getInstance(key.encoded)
return findSignatureScheme(keyInfo.privateKeyAlgorithm)
} }
/** /**
@ -217,19 +275,9 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey { fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm) val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
// There are cases where the same key algorithm is applied to different signature schemes. val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm)
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
// to generate the appropriate key.
for (signatureScheme in algorithmGroups[algorithm]!!) {
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
}
}
throw IllegalArgumentException("This private key cannot be decoded, please ensure it is PKCS8 encoded and the signature scheme is supported.")
} }
/** /**
@ -270,19 +318,9 @@ object Crypto {
*/ */
@Throws(IllegalArgumentException::class) @Throws(IllegalArgumentException::class)
fun decodePublicKey(encodedKey: ByteArray): PublicKey { fun decodePublicKey(encodedKey: ByteArray): PublicKey {
val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm) val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
// There are cases where the same key algorithm is applied to different signature schemes. val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves. return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
// In such a case, we should try and identify which of the candidate schemes is the correct one so as
// to generate the appropriate key.
for (signatureScheme in algorithmGroups[algorithm]!!) {
try {
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
} catch (ikse: InvalidKeySpecException) {
// ignore it - only used to bypass the scheme that causes an exception, as it has the same name, but different params.
}
}
throw IllegalArgumentException("This public key cannot be decoded, please ensure it is X509 encoded and the signature scheme is supported.")
} }
/** /**
@ -527,7 +565,7 @@ object Crypto {
if (signatureScheme.algSpec != null) if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom()) keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
else else
keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom()) keyPairGenerator.initialize(signatureScheme.keySize!!, newSecureRandom())
return keyPairGenerator.generateKeyPair() return keyPairGenerator.generateKeyPair()
} }
@ -834,16 +872,6 @@ object Crypto {
/** Check if the requested [SignatureScheme] is supported by the system. */ /** Check if the requested [SignatureScheme] is supported by the system. */
fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme fun isSupportedSignatureScheme(signatureScheme: SignatureScheme): Boolean = supportedSignatureSchemes[signatureScheme.schemeCodeName] === signatureScheme
// map algorithm names returned from Keystore (or after encode/decode) to the supported algorithm names.
private fun matchingAlgorithmName(algorithm: String): String {
return when (algorithm) {
"EC" -> "ECDSA"
"SPHINCS-256" -> "SPHINCS256"
"1.3.6.1.4.1.22554.2.1" -> "SPHINCS256" // Unfortunately, PKCS8Key and X509Key parsing return the OID as the algorithm name and not SPHINCS256.
else -> algorithm
}
}
// validate a key, by checking its algorithmic params. // validate a key, by checking its algorithmic params.
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean { private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
return when (key) { return when (key) {

View File

@ -2,42 +2,12 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.identity.AnonymousParty import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party import net.corda.core.utilities.OpaqueBytes
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import java.math.BigInteger import java.math.BigInteger
import net.corda.core.utilities.SgxSupport import net.corda.core.utilities.SgxSupport
import java.security.* import java.security.*
@CordaSerializable
object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "NULL"
override fun getEncoded() = byteArrayOf(0)
override fun getFormat() = "NULL"
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
override fun toString() = "NULL_KEY"
}
val NULL_PARTY = AnonymousParty(NullPublicKey)
// TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable
@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead")
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
override fun hashCode(): Int = s.hashCode()
override fun toString() = "PUBKEY[$s]"
}
/** 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. * Utility to simplify the act of signing a byte array.
* @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root). * @param bytesToSign the data/message to be signed in [ByteArray] form (usually the Merkle root).
@ -66,17 +36,6 @@ fun PrivateKey.sign(bytesToSign: ByteArray, publicKey: PublicKey): DigitalSignat
@Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class) @Throws(IllegalArgumentException::class, InvalidKeyException::class, SignatureException::class)
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public) fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, public)
fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public) fun KeyPair.sign(bytesToSign: OpaqueBytes) = private.sign(bytesToSign.bytes, public)
fun KeyPair.sign(bytesToSign: OpaqueBytes, party: Party) = sign(bytesToSign.bytes, party)
// TODO This case will need more careful thinking, as party owningKey can be a CompositeKey. One way of doing that is
// implementation of CompositeSignature.
@Throws(InvalidKeyException::class)
fun KeyPair.sign(bytesToSign: ByteArray, party: Party): DigitalSignature.LegallyIdentifiable {
// Quick workaround when we have CompositeKey as Party owningKey.
if (party.owningKey is CompositeKey) throw InvalidKeyException("Signing for parties with CompositeKey not supported.")
val sig = sign(bytesToSign)
return DigitalSignature.LegallyIdentifiable(party, sig.bytes)
}
/** /**
* Utility to simplify the act of verifying a signature. * Utility to simplify the act of verifying a signature.
@ -262,3 +221,17 @@ private val _newSecureRandom: () -> SecureRandom by lazy {
*/ */
@Throws(NoSuchAlgorithmException::class) @Throws(NoSuchAlgorithmException::class)
fun newSecureRandom() = _newSecureRandom() fun newSecureRandom() = _newSecureRandom()
/**
* Returns a random positive non-zero long generated using a secure RNG. This function sacrifies a bit of entropy in order
* to avoid potential bugs where the value is used in a context where negative numbers or zero are not expected.
*/
fun random63BitValue(): Long {
while (true) {
val candidate = Math.abs(newSecureRandom().nextLong())
// No need to check for -0L
if (candidate != 0L && candidate != Long.MIN_VALUE) {
return candidate
}
}
}

View File

@ -1,8 +1,7 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import java.security.InvalidKeyException import java.security.InvalidKeyException
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException import java.security.SignatureException
@ -46,7 +45,4 @@ open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
@Throws(InvalidKeyException::class, SignatureException::class) @Throws(InvalidKeyException::class, SignatureException::class)
fun isValid(content: ByteArray) = by.isValid(content, this) fun isValid(content: ByteArray) = by.isValid(content, this)
} }
// TODO: consider removing this as whoever needs to identify the signer should be able to derive it from the public key
class LegallyIdentifiable(val signer: Party, bits: ByteArray) : WithKey(signer.owningKey, bits)
} }

View File

@ -65,4 +65,4 @@ fun String.hexToBase64(): String = hexToByteArray().toBase64()
// structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519). // structure, e.g. mapping a PublicKey to a condition with the specific feature (ED25519).
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>() fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58() fun PublicKey.toBase58String(): String = this.serialize().bytes.toBase58()
fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes fun PublicKey.toSHA256Bytes(): ByteArray = this.serialize().bytes.sha256().bytes // TODO: decide on the format of hashed key (encoded Vs serialised).

View File

@ -1,7 +1,7 @@
package net.corda.core.crypto package net.corda.core.crypto
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.opaque import net.corda.core.utilities.opaque
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import java.security.PublicKey import java.security.PublicKey
import java.time.Instant import java.time.Instant

View File

@ -2,7 +2,7 @@ package net.corda.core.crypto
import com.google.common.io.BaseEncoding import com.google.common.io.BaseEncoding
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import java.security.MessageDigest import java.security.MessageDigest
/** /**

View File

@ -1,6 +1,6 @@
package net.corda.core.crypto package net.corda.core.crypto
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.Signature import java.security.Signature
import java.security.spec.AlgorithmParameterSpec import java.security.spec.AlgorithmParameterSpec
@ -8,7 +8,9 @@ import java.security.spec.AlgorithmParameterSpec
* This class is used to define a digital signature scheme. * 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 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 schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
* @param signatureOID object identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA) * @param signatureOID ASN.1 algorithm identifier of the signature algorithm (e.g 1.3.101.112 for EdDSA)
* @param alternativeOIDs ASN.1 algorithm identifiers for keys of the signature, where we want to map multiple keys to
* the same signature scheme.
* @param providerName the provider's name (e.g. "BC"). * @param providerName the provider's name (e.g. "BC").
* @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256). * @param algorithmName which signature algorithm is used (e.g. RSA, ECDSA. EdDSA, SPHINCS-256).
* @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA") * @param signatureName a signature-scheme name as required to create [Signature] objects (e.g. "SHA256withECDSA")
@ -20,10 +22,11 @@ import java.security.spec.AlgorithmParameterSpec
data class SignatureScheme( data class SignatureScheme(
val schemeNumberID: Int, val schemeNumberID: Int,
val schemeCodeName: String, val schemeCodeName: String,
val signatureOID: ASN1ObjectIdentifier, val signatureOID: AlgorithmIdentifier,
val alternativeOIDs: List<AlgorithmIdentifier>,
val providerName: String, val providerName: String,
val algorithmName: String, val algorithmName: String,
val signatureName: String, val signatureName: String,
val algSpec: AlgorithmParameterSpec?, val algSpec: AlgorithmParameterSpec?,
val keySize: Int, val keySize: Int?,
val desc: String) val desc: String)

View File

@ -0,0 +1,277 @@
package net.corda.core.crypto.composite
import net.corda.core.crypto.Crypto
import net.corda.core.crypto.composite.CompositeKey.NodeAndWeight
import net.corda.core.crypto.keys
import net.corda.core.crypto.provider.CordaObjectIdentifier
import net.corda.core.crypto.toSHA256Bytes
import net.corda.core.crypto.toStringShort
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import java.nio.ByteBuffer
import java.security.PublicKey
import java.util.*
/**
* A tree data structure that enables the representation of composite public keys.
* Notice that with that implementation CompositeKey extends PublicKey. Leaves are represented by single public keys.
*
* For complex scenarios, such as *"Both Alice and Bob need to sign to consume a state S"*, we can represent
* the requirement by creating a tree with a root [CompositeKey], and Alice and Bob as children.
* The root node would specify *weights* for each of its children and a *threshold* the minimum total weight required
* (e.g. the minimum number of child signatures required) to satisfy the tree signature requirement.
*
* 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"*.
*
* [CompositeKey] maintains a list of [NodeAndWeight]s which holds child subtree with associated weight carried by child node signatures.
*
* The [threshold] specifies the minimum total weight required (in the simple case the minimum number of child
* signatures required) to satisfy the sub-tree rooted at this node.
*/
@CordaSerializable
class CompositeKey private constructor(val threshold: Int, children: List<NodeAndWeight>) : PublicKey {
companion object {
val KEY_ALGORITHM = "COMPOSITE"
/**
* Build a composite key from a DER encoded form.
*/
fun getInstance(encoded: ByteArray) = getInstance(ASN1Primitive.fromByteArray(encoded))
fun getInstance(asn1: ASN1Primitive): PublicKey {
val keyInfo = SubjectPublicKeyInfo.getInstance(asn1)
require(keyInfo.algorithm.algorithm == CordaObjectIdentifier.compositeKey)
val sequence = ASN1Sequence.getInstance(keyInfo.parsePublicKey())
val threshold = ASN1Integer.getInstance(sequence.getObjectAt(0)).positiveValue.toInt()
val sequenceOfChildren = ASN1Sequence.getInstance(sequence.getObjectAt(1))
val builder = Builder()
val listOfChildren = sequenceOfChildren.objects.toList()
listOfChildren.forEach { childAsn1 ->
require(childAsn1 is ASN1Sequence)
val childSeq = childAsn1 as ASN1Sequence
val key = Crypto.decodePublicKey((childSeq.getObjectAt(0) as DERBitString).bytes)
val weight = ASN1Integer.getInstance(childSeq.getObjectAt(1))
builder.addKey(key, weight.positiveValue.toInt())
}
return builder.build(threshold)
}
}
val children = children.sorted()
init {
// TODO: replace with the more extensive, but slower, checkValidity() test.
checkConstraints()
}
@Transient
private var validated = false
// Check for key duplication, threshold and weight constraints and test for aggregated weight integer overflow.
private fun checkConstraints() {
require(children.size == children.toSet().size) { "CompositeKey with duplicated child nodes detected." }
// If we want PublicKey we only keep one key, otherwise it will lead to semantically equivalent trees
// but having different structures.
require(children.size > 1) { "CompositeKey must consist of two or more child nodes." }
// We should ensure threshold is positive, because smaller allowable weight for a node key is 1.
require(threshold > 0) { "CompositeKey threshold is set to $threshold, but it should be a positive integer." }
// If threshold is bigger than total weight, then it will never be satisfied.
val totalWeight = totalWeight()
require(threshold <= totalWeight) {
"CompositeKey threshold: $threshold cannot be bigger than aggregated weight of child nodes: $totalWeight"
}
}
// Graph cycle detection in the composite key structure to avoid infinite loops on CompositeKey graph traversal and
// when recursion is used (i.e. in isFulfilledBy()).
// An IdentityHashMap Vs HashMap is used, because a graph cycle causes infinite loop on the CompositeKey.hashCode().
private fun cycleDetection(visitedMap: IdentityHashMap<CompositeKey, Boolean>) {
for ((node) in children) {
if (node is CompositeKey) {
val curVisitedMap = IdentityHashMap<CompositeKey, Boolean>()
curVisitedMap.putAll(visitedMap)
require(!curVisitedMap.contains(node)) { "Cycle detected for CompositeKey: $node" }
curVisitedMap.put(node, true)
node.cycleDetection(curVisitedMap)
}
}
}
/**
* This method will detect graph cycles in the full composite key structure to protect against infinite loops when
* traversing the graph and key duplicates in the each layer. It also checks if the threshold and weight constraint
* requirements are met, while it tests for aggregated-weight integer overflow.
* In practice, this method should be always invoked on the root [CompositeKey], as it inherently
* validates the child nodes (all the way till the leaves).
* TODO: Always call this method when deserialising [CompositeKey]s.
*/
fun checkValidity() {
val visitedMap = IdentityHashMap<CompositeKey, Boolean>()
visitedMap.put(this, true)
cycleDetection(visitedMap) // Graph cycle testing on the root node.
checkConstraints()
for ((node, _) in children) {
if (node is CompositeKey) {
// We don't need to check for cycles on the rest of the nodes (testing on the root node is enough).
node.checkConstraints()
}
}
validated = true
}
// Method to check if the total (aggregated) weight of child nodes overflows.
// Unlike similar solutions that use long conversion, this approach takes advantage of the minimum weight being 1.
private fun totalWeight(): Int {
var sum = 0
for ((_, weight) in children) {
require(weight > 0) { "Non-positive weight: $weight detected." }
sum = Math.addExact(sum, weight) // Add and check for integer overflow.
}
return sum
}
/**
* Holds node - weight pairs for a CompositeKey. Ordered first by weight, then by node's hashCode.
* Each node should be assigned with a positive weight to avoid certain types of weight underflow attacks.
*/
@CordaSerializable
data class NodeAndWeight(val node: PublicKey, val weight: Int) : Comparable<NodeAndWeight>, ASN1Object() {
init {
// We don't allow zero or negative weights. Minimum weight = 1.
require(weight > 0) { "A non-positive weight was detected. Node info: $this" }
}
override fun compareTo(other: NodeAndWeight): Int {
return if (weight == other.weight)
ByteBuffer.wrap(node.toSHA256Bytes()).compareTo(ByteBuffer.wrap(other.node.toSHA256Bytes()))
else
weight.compareTo(other.weight)
}
override fun toASN1Primitive(): ASN1Primitive {
val vector = ASN1EncodableVector()
vector.add(DERBitString(node.encoded))
vector.add(ASN1Integer(weight.toLong()))
return DERSequence(vector)
}
override fun toString(): String {
return "Public key: ${node.toStringShort()}, weight: $weight"
}
}
/**
* Takes single PublicKey and checks if CompositeKey requirements hold for that key.
*/
fun isFulfilledBy(key: PublicKey) = isFulfilledBy(setOf(key))
override fun getAlgorithm() = KEY_ALGORITHM
override fun getEncoded(): ByteArray {
val keyVector = ASN1EncodableVector()
val childrenVector = ASN1EncodableVector()
children.forEach {
childrenVector.add(it.toASN1Primitive())
}
keyVector.add(ASN1Integer(threshold.toLong()))
keyVector.add(DERSequence(childrenVector))
return SubjectPublicKeyInfo(AlgorithmIdentifier(CordaObjectIdentifier.compositeKey), DERSequence(keyVector)).encoded
}
override fun getFormat() = ASN1Encoding.DER
// Extracted method from isFulfilledBy.
private fun checkFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
if (keysToCheck.any { it is CompositeKey }) return false
val totalWeight = children.map { (node, weight) ->
if (node is CompositeKey) {
if (node.checkFulfilledBy(keysToCheck)) weight else 0
} else {
if (keysToCheck.contains(node)) weight else 0
}
}.sum()
return totalWeight >= threshold
}
/**
* Function checks if the public keys corresponding to the signatures are matched against the leaves of the composite
* key tree in question, and the total combined weight of all children is calculated for every intermediary node.
* If all thresholds are satisfied, the composite key requirement is considered to be met.
*/
fun isFulfilledBy(keysToCheck: Iterable<PublicKey>): Boolean {
// We validate keys only when checking if they're matched, as this checks subkeys as a result.
// Doing these checks at deserialization/construction time would result in duplicate checks.
if (!validated)
checkValidity() // TODO: remove when checkValidity() will be eventually invoked during/after deserialization.
return checkFulfilledBy(keysToCheck)
}
/**
* Set of all leaf keys of that CompositeKey.
*/
val leafKeys: Set<PublicKey>
get() = children.flatMap { it.node.keys }.toSet() // Uses PublicKey.keys extension.
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is CompositeKey) return false
if (threshold != other.threshold) return false
if (children != other.children) return false
return true
}
override fun hashCode(): Int {
var result = threshold
result = 31 * result + children.hashCode()
return result
}
override fun toString() = "(${children.joinToString()})"
/** A helper class for building a [CompositeKey]. */
class Builder {
private val children: MutableList<NodeAndWeight> = mutableListOf()
/** Adds a child [CompositeKey] node. Specifying a [weight] for the child is optional and will default to 1. */
fun addKey(key: PublicKey, weight: Int = 1): Builder {
children.add(NodeAndWeight(key, weight))
return this
}
fun addKeys(vararg keys: PublicKey): Builder {
keys.forEach { addKey(it) }
return this
}
fun addKeys(keys: List<PublicKey>): Builder = addKeys(*keys.toTypedArray())
/**
* Builds the [CompositeKey]. If [threshold] is not specified, it will default to
* the total (aggregated) weight of the children, effectively generating an "N of N" requirement.
* During process removes single keys wrapped in [CompositeKey] and enforces ordering on child nodes.
*
* @throws IllegalArgumentException
*/
fun build(threshold: Int? = null): PublicKey {
val n = children.size
return if (n > 1)
CompositeKey(threshold ?: children.map { (_, weight) -> weight }.sum(), children)
else if (n == 1) {
require(threshold == null || threshold == children.first().weight)
{ "Trying to build invalid CompositeKey, threshold value different than weight of single child node." }
children.first().node // We can assume that this node is a correct CompositeKey.
} else throw IllegalArgumentException("Trying to build CompositeKey without child nodes.")
}
}
}
/**
* Expands all [CompositeKey]s present in PublicKey iterable to set of single [PublicKey]s.
* If an element of the set is a single PublicKey it gives just that key, if it is a [CompositeKey] it returns all leaf
* keys for that composite element.
*/
val Iterable<PublicKey>.expandedCompositeKeys: Set<PublicKey>
get() = flatMap { it.keys }.toSet()

View File

@ -1,4 +1,4 @@
package net.corda.core.crypto package net.corda.core.crypto.composite
import net.corda.core.serialization.deserialize import net.corda.core.serialization.deserialize
import org.bouncycastle.asn1.ASN1ObjectIdentifier import org.bouncycastle.asn1.ASN1ObjectIdentifier
@ -10,14 +10,10 @@ import java.security.spec.AlgorithmParameterSpec
/** /**
* Dedicated class for storing a set of signatures that comprise [CompositeKey]. * Dedicated class for storing a set of signatures that comprise [CompositeKey].
*/ */
class CompositeSignature : Signature(ALGORITHM) { class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
companion object { companion object {
val ALGORITHM = "2.25.30086077608615255153862931087626791003" val SIGNATURE_ALGORITHM = "COMPOSITESIG"
// UUID-based OID fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
// TODO: Register for an OID space and issue our own shorter OID
val ALGORITHM_IDENTIFIER = AlgorithmIdentifier(ASN1ObjectIdentifier(ALGORITHM))
fun getService(provider: Provider) = Provider.Service(provider, "Signature", ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
} }
private var signatureState: State? = null private var signatureState: State? = null

View File

@ -1,5 +1,6 @@
package net.corda.core.crypto package net.corda.core.crypto.composite
import net.corda.core.crypto.DigitalSignature
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
/** /**

View File

@ -0,0 +1,34 @@
package net.corda.core.crypto.composite
import java.security.*
import java.security.spec.InvalidKeySpecException
import java.security.spec.KeySpec
import java.security.spec.X509EncodedKeySpec
class KeyFactory : KeyFactorySpi() {
@Throws(InvalidKeySpecException::class)
override fun engineGeneratePrivate(keySpec: KeySpec): PrivateKey {
// Private composite key not supported.
throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass)
}
@Throws(InvalidKeySpecException::class)
override fun engineGeneratePublic(keySpec: KeySpec): PublicKey? {
return when (keySpec) {
is X509EncodedKeySpec -> CompositeKey.getInstance(keySpec.encoded)
else -> throw InvalidKeySpecException("key spec not recognised: " + keySpec.javaClass)
}
}
@Throws(InvalidKeySpecException::class)
override fun <T : KeySpec> engineGetKeySpec(key: Key, keySpec: Class<T>): T {
// Only support [X509EncodedKeySpec].
throw InvalidKeySpecException("Not implemented yet $key $keySpec")
}
@Throws(InvalidKeyException::class)
override fun engineTranslateKey(key: Key): Key {
throw InvalidKeyException("No other composite key providers known")
}
}

View File

@ -0,0 +1,37 @@
package net.corda.core.crypto.provider
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.composite.CompositeSignature
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.AccessController
import java.security.PrivilegedAction
import java.security.Provider
class CordaSecurityProvider : Provider(PROVIDER_NAME, 0.1, "$PROVIDER_NAME security provider wrapper") {
companion object {
val PROVIDER_NAME = "Corda"
}
init {
AccessController.doPrivileged(PrivilegedAction<Unit> { setup() })
}
private fun setup() {
put("KeyFactory.${CompositeKey.KEY_ALGORITHM}", "net.corda.core.crypto.composite.KeyFactory")
put("Signature.${CompositeSignature.SIGNATURE_ALGORITHM}", "net.corda.core.crypto.composite.CompositeSignature")
val compositeKeyOID = CordaObjectIdentifier.compositeKey.id
put("Alg.Alias.KeyFactory.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.KeyFactory.OID.$compositeKeyOID", CompositeKey.KEY_ALGORITHM)
put("Alg.Alias.Signature.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
put("Alg.Alias.Signature.OID.$compositeKeyOID", CompositeSignature.SIGNATURE_ALGORITHM)
}
}
object CordaObjectIdentifier {
// UUID-based OID
// TODO: Register for an OID space and issue our own shorter OID
val compositeKey = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791002")
val compositeSignature = ASN1ObjectIdentifier("2.25.30086077608615255153862931087626791003")
}

View File

@ -0,0 +1,35 @@
package net.corda.core.crypto.testing
import net.corda.core.crypto.DigitalSignature
import net.corda.core.identity.AnonymousParty
import net.corda.core.serialization.CordaSerializable
import java.math.BigInteger
import java.security.PublicKey
@CordaSerializable
object NullPublicKey : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "NULL"
override fun getEncoded() = byteArrayOf(0)
override fun getFormat() = "NULL"
override fun compareTo(other: PublicKey): Int = if (other == NullPublicKey) 0 else -1
override fun toString() = "NULL_KEY"
}
val NULL_PARTY = AnonymousParty(NullPublicKey)
// TODO: Clean up this duplication between Null and Dummy public key
@CordaSerializable
@Deprecated("Has encoding format problems, consider entropyToKeyPair() instead")
class DummyPublicKey(val s: String) : PublicKey, Comparable<PublicKey> {
override fun getAlgorithm() = "DUMMY"
override fun getEncoded() = s.toByteArray()
override fun getFormat() = "ASN.1"
override fun compareTo(other: PublicKey): Int = BigInteger(encoded).compareTo(BigInteger(other.encoded))
override fun equals(other: Any?) = other is DummyPublicKey && other.s == s
override fun hashCode(): Int = s.hashCode()
override fun toString() = "PUBKEY[$s]"
}
/** 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))

View File

@ -1,7 +1,7 @@
package net.corda.core.flows package net.corda.core.flows
import net.corda.core.utilities.CordaException import net.corda.core.CordaException
import net.corda.core.utilities.CordaRuntimeException import net.corda.core.CordaRuntimeException
// DOCSTART 1 // DOCSTART 1
/** /**

View File

@ -4,13 +4,13 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine import net.corda.core.internal.FlowStateMachine
import net.corda.core.messaging.DataFeed
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug import net.corda.core.utilities.debug
import org.slf4j.Logger import org.slf4j.Logger
import rx.Observable
/** /**
* A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you * A sub-class of [FlowLogic<T>] implements a flow using direct, straight line blocking code. Thus you
@ -180,7 +180,7 @@ abstract class FlowLogic<out T> {
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded. * @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded.
*/ */
@Throws(FlowException::class) @Throws(FlowException::class)
fun checkFlowPermission(permissionName: String, extraAuditData: Map<String,String>) = stateMachine.checkFlowPermission(permissionName, extraAuditData) fun checkFlowPermission(permissionName: String, extraAuditData: Map<String, String>) = stateMachine.checkFlowPermission(permissionName, extraAuditData)
/** /**
@ -189,7 +189,7 @@ abstract class FlowLogic<out T> {
* @param comment a general human readable summary of the event. * @param comment a general human readable summary of the event.
* @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded. * @param extraAuditData in the audit log for this permission check these extra key value pairs will be recorded.
*/ */
fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String,String>) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData) fun recordAuditEvent(eventType: String, comment: String, extraAuditData: Map<String, String>) = stateMachine.recordAuditEvent(eventType, comment, extraAuditData)
/** /**
* Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something * Override this to provide a [ProgressTracker]. If one is provided and stepped, the framework will do something
@ -215,10 +215,10 @@ abstract class FlowLogic<out T> {
* *
* @return Returns null if this flow has no progress tracker. * @return Returns null if this flow has no progress tracker.
*/ */
fun track(): Pair<String, Observable<String>>? { fun track(): DataFeed<String, String>? {
// TODO this is not threadsafe, needs an atomic get-step-and-subscribe // TODO this is not threadsafe, needs an atomic get-step-and-subscribe
return progressTracker?.let { return progressTracker?.let {
it.currentStep.label to it.changes.map { it.toString() } DataFeed(it.currentStep.label, it.changes.map { it.toString() })
} }
} }
@ -230,7 +230,7 @@ abstract class FlowLogic<out T> {
@Suspendable @Suspendable
fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this) fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this)
//////////////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////////////////
private var _stateMachine: FlowStateMachine<*>? = null private var _stateMachine: FlowStateMachine<*>? = null
/** /**

View File

@ -4,8 +4,10 @@ import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.reflect.KClass import kotlin.reflect.KClass
/** /**
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that * This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The class must
* does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow]. * have at least a constructor which takes in a single [net.corda.core.identity.Party] parameter which represents the
* initiating counterparty. The [FlowLogic] that does the initiating is specified by the [value] property and itself must be annotated
* with [InitiatingFlow].
* *
* The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating * The node on startup scans for [FlowLogic]s which are annotated with this and automatically registers the initiating
* to initiated flow mapping. * to initiated flow mapping.

View File

@ -2,7 +2,7 @@ package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey import java.security.PublicKey

View File

@ -0,0 +1,16 @@
package net.corda.flows
import net.corda.core.identity.AnonymousParty
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.cert.X509CertificateHolder
import java.security.PublicKey
import java.security.cert.CertPath
@CordaSerializable
data class AnonymisedIdentity(
val certPath: CertPath,
val certificate: X509CertificateHolder,
val identity: AnonymousParty) {
constructor(certPath: CertPath, certificate: X509CertificateHolder, identity: PublicKey)
: this(certPath, certificate, AnonymousParty(identity))
}

View File

@ -2,7 +2,7 @@ package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey import java.security.PublicKey

View File

@ -2,9 +2,7 @@ package net.corda.core.identity
import net.corda.core.contracts.PartyAndReference import net.corda.core.contracts.PartyAndReference
import net.corda.core.crypto.CertificateAndKeyPair import net.corda.core.crypto.CertificateAndKeyPair
import net.corda.core.crypto.toBase58String import net.corda.core.utilities.OpaqueBytes
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.security.PublicKey import java.security.PublicKey

View File

@ -1,7 +1,6 @@
package net.corda.core.messaging package net.corda.core.messaging
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.ErrorOr
import net.corda.core.contracts.Amount import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateAndRef import net.corda.core.contracts.StateAndRef
@ -10,16 +9,19 @@ import net.corda.core.crypto.SecureHash
import net.corda.core.flows.FlowInitiator import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.FlowLogic import net.corda.core.flows.FlowLogic
import net.corda.core.flows.StateMachineRunId import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.VaultQueryException
import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE
import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.Sort
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Try
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import rx.Observable import rx.Observable
import java.io.InputStream import java.io.InputStream
@ -32,7 +34,7 @@ data class StateMachineInfo(
val id: StateMachineRunId, val id: StateMachineRunId,
val flowLogicClassName: String, val flowLogicClassName: String,
val initiator: FlowInitiator, val initiator: FlowInitiator,
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>? val progressTrackerStepAndUpdates: DataFeed<String, String>?
) { ) {
override fun toString(): String = "${javaClass.simpleName}($id, $flowLogicClassName)" override fun toString(): String = "${javaClass.simpleName}($id, $flowLogicClassName)"
} }
@ -45,16 +47,16 @@ sealed class StateMachineUpdate {
override val id: StateMachineRunId get() = stateMachineInfo.id override val id: StateMachineRunId get() = stateMachineInfo.id
} }
data class Removed(override val id: StateMachineRunId, val result: ErrorOr<*>) : StateMachineUpdate() data class Removed(override val id: StateMachineRunId, val result: Try<*>) : StateMachineUpdate()
} }
@CordaSerializable
data class StateMachineTransactionMapping(val stateMachineRunId: StateMachineRunId, val transactionId: SecureHash)
/** /**
* RPC operations that the node exposes to clients using the Java client library. These can be called from * RPC operations that the node exposes to clients using the Java client library. These can be called from
* client apps and are implemented by the node in the [net.corda.node.internal.CordaRPCOpsImpl] class. * client apps and are implemented by the node in the [net.corda.node.internal.CordaRPCOpsImpl] class.
*/ */
// TODO: The use of Pairs throughout is unfriendly for Java interop.
interface CordaRPCOps : RPCOps { interface CordaRPCOps : RPCOps {
/** /**
* Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed * Returns the RPC protocol version, which is the same the node's Platform Version. Exists since version 1 so guaranteed
@ -63,10 +65,13 @@ interface CordaRPCOps : RPCOps {
override val protocolVersion: Int get() = nodeIdentity().platformVersion override val protocolVersion: Int get() = nodeIdentity().platformVersion
/** /**
* Returns a pair of currently in-progress state machine infos and an observable of future state machine adds/removes. * Returns a data feed of currently in-progress state machine infos and an observable of future state machine adds/removes.
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun stateMachinesAndUpdates(): Pair<List<StateMachineInfo>, Observable<StateMachineUpdate>> fun stateMachinesFeed(): DataFeed<List<StateMachineInfo>, StateMachineUpdate>
@Deprecated("This function will be removed in a future milestone", ReplaceWith("stateMachinesFeed()"))
fun stateMachinesAndUpdates() = stateMachinesFeed()
/** /**
* Returns a snapshot of vault states for a given query criteria (and optional order and paging specification) * Returns a snapshot of vault states for a given query criteria (and optional order and paging specification)
@ -76,11 +81,18 @@ interface CordaRPCOps : RPCOps {
* and returns a [Vault.Page] object containing the following: * and returns a [Vault.Page] object containing the following:
* 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification]) * 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification])
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
* 3. the [PageSpecification] used in the query * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
* 4. a total number of results available (for subsequent paging if necessary) * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL
* 5. other results (aggregate functions with/without using value groups)
* *
* Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. * @throws VaultQueryException if the query cannot be executed for any reason
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. * (missing criteria or parsing error, paging errors, unsupported query, underlying database error)
*
* Notes
* If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned.
* API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results,
* otherwise a [VaultQueryException] will be thrown alerting to this condition.
* It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification].
*/ */
// DOCSTART VaultQueryByAPI // DOCSTART VaultQueryByAPI
@RPCReturnsObservables @RPCReturnsObservables
@ -119,59 +131,69 @@ interface CordaRPCOps : RPCOps {
* *
* Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function. * Notes: the snapshot part of the query adheres to the same behaviour as the [queryBy] function.
* the [QueryCriteria] applies to both snapshot and deltas (streaming updates). * the [QueryCriteria] applies to both snapshot and deltas (streaming updates).
*/ */
// DOCSTART VaultTrackByAPI // DOCSTART VaultTrackByAPI
@RPCReturnsObservables @RPCReturnsObservables
fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria, fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria,
paging: PageSpecification, paging: PageSpecification,
sorting: Sort, sorting: Sort,
contractType: Class<out T>): Vault.PageAndUpdates<T> contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update>
// DOCEND VaultTrackByAPI // DOCEND VaultTrackByAPI
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Note: cannot apply @JvmOverloads to interfaces nor interface implementations
// Java Helpers // Java Helpers
// DOCSTART VaultTrackAPIHelpers // DOCSTART VaultTrackAPIHelpers
fun <T : ContractState> vaultTrack(contractType: Class<out T>): Vault.PageAndUpdates<T> { fun <T : ContractState> vaultTrack(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update> {
return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) return vaultTrackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
} }
fun <T : ContractState> vaultTrackByCriteria(contractType: Class<out T>, criteria: QueryCriteria): Vault.PageAndUpdates<T> { fun <T : ContractState> vaultTrackByCriteria(contractType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update> {
return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) return vaultTrackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
} }
fun <T : ContractState> vaultTrackByWithPagingSpec(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> { fun <T : ContractState> vaultTrackByWithPagingSpec(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update> {
return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType) return vaultTrackBy(criteria, paging, Sort(emptySet()), contractType)
} }
fun <T : ContractState> vaultTrackByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> { fun <T : ContractState> vaultTrackByWithSorting(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update> {
return vaultTrackBy(criteria, PageSpecification(), sorting, contractType) return vaultTrackBy(criteria, PageSpecification(), sorting, contractType)
} }
// DOCEND VaultTrackAPIHelpers // DOCEND VaultTrackAPIHelpers
/** /**
* Returns a pair of head states in the vault and an observable of future updates to the vault. * Returns a data feed of head states in the vault and an observable of future updates to the vault.
*/ */
@RPCReturnsObservables @RPCReturnsObservables
// TODO: Remove this from the interface // TODO: Remove this from the interface
@Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())")) @Deprecated("This function will be removed in a future milestone", ReplaceWith("vaultTrackBy(QueryCriteria())"))
fun vaultAndUpdates(): Pair<List<StateAndRef<ContractState>>, Observable<Vault.Update>> fun vaultAndUpdates(): DataFeed<List<StateAndRef<ContractState>>, Vault.Update>
/** /**
* Returns a pair of all recorded transactions and an observable of future recorded ones. * Returns a data feed of all recorded transactions and an observable of future recorded ones.
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun verifiedTransactions(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> fun verifiedTransactionsFeed(): DataFeed<List<SignedTransaction>, SignedTransaction>
@Deprecated("This function will be removed in a future milestone", ReplaceWith("verifiedTransactionFeed()"))
fun verifiedTransactions() = verifiedTransactionsFeed()
/** /**
* Returns a snapshot list of existing state machine id - recorded transaction hash mappings, and a stream of future * Returns a snapshot list of existing state machine id - recorded transaction hash mappings, and a stream of future
* such mappings as well. * such mappings as well.
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun stateMachineRecordedTransactionMapping(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>> fun stateMachineRecordedTransactionMappingFeed(): DataFeed<List<StateMachineTransactionMapping>, StateMachineTransactionMapping>
@Deprecated("This function will be removed in a future milestone", ReplaceWith("stateMachineRecordedTransactionMappingFeed()"))
fun stateMachineRecordedTransactionMapping() = stateMachineRecordedTransactionMappingFeed()
/** /**
* Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network. * Returns all parties currently visible on the network with their advertised services and an observable of future updates to the network.
*/ */
@RPCReturnsObservables @RPCReturnsObservables
fun networkMapUpdates(): Pair<List<NodeInfo>, Observable<NetworkMapCache.MapChange>> fun networkMapFeed(): DataFeed<List<NodeInfo>, NetworkMapCache.MapChange>
@Deprecated("This function will be removed in a future milestone", ReplaceWith("networkMapFeed()"))
fun networkMapUpdates() = networkMapFeed()
/** /**
* Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC]. * Start the given flow with the given arguments. [logicType] must be annotated with [net.corda.core.flows.StartableByRPC].
@ -282,6 +304,13 @@ interface CordaRPCOps : RPCOps {
/** Enumerates the class names of the flows that this node knows about. */ /** Enumerates the class names of the flows that this node knows about. */
fun registeredFlows(): List<String> fun registeredFlows(): List<String>
/**
* Returns a node's identity from the network map cache, where known.
*
* @return the node info if available.
*/
fun nodeIdentityFromParty(party: AbstractParty): NodeInfo?
} }
inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
@ -292,7 +321,7 @@ inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryC
inline fun <reified T : ContractState> CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(), inline fun <reified T : ContractState> CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
paging: PageSpecification = PageSpecification(), paging: PageSpecification = PageSpecification(),
sorting: Sort = Sort(emptySet())): Vault.PageAndUpdates<T> { sorting: Sort = Sort(emptySet())): DataFeed<Vault.Page<T>, Vault.Update> {
return vaultTrackBy(criteria, paging, sorting, T::class.java) return vaultTrackBy(criteria, paging, sorting, T::class.java)
} }
@ -340,6 +369,27 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlow
arg3: D arg3: D
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) ): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
inline fun <T : Any, A, B, C, D, E, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
@Suppress("UNUSED_PARAMETER")
flowConstructor: (A, B, C, D, E) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4)
inline fun <T : Any, A, B, C, D, E, F, reified R : FlowLogic<T>> CordaRPCOps.startFlow(
@Suppress("UNUSED_PARAMETER")
flowConstructor: (A, B, C, D, E, F) -> R,
arg0: A,
arg1: B,
arg2: C,
arg3: D,
arg4: E,
arg5: F
): FlowHandle<T> = startFlowDynamic(R::class.java, arg0, arg1, arg2, arg3, arg4, arg5)
/** /**
* Same again, except this time with progress-tracking enabled. * Same again, except this time with progress-tracking enabled.
*/ */
@ -382,3 +432,18 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
arg2: C, arg2: C,
arg3: D arg3: D
): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3) ): FlowProgressHandle<T> = startTrackedFlowDynamic(R::class.java, arg0, arg1, arg2, arg3)
/**
* The Data feed contains a snapshot of the requested data and an [Observable] of future updates.
*/
@CordaSerializable
data class DataFeed<out A, B>(val snapshot: A, val updates: Observable<B>) {
@Deprecated("This function will be removed in a future milestone", ReplaceWith("snapshot"))
val first: A get() = snapshot
@Deprecated("This function will be removed in a future milestone", ReplaceWith("updates"))
val second: Observable<B> get() = updates
@Deprecated("This function will be removed in a future milestone", ReplaceWith("snapshot"))
val current: A get() = snapshot
@Deprecated("This function will be removed in a future milestone", ReplaceWith("updates"))
val future: Observable<B> get() = updates
}

View File

@ -2,11 +2,10 @@ package net.corda.core.node
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.cert.X509CertificateHolder import net.corda.core.utilities.NetworkHostAndPort
/** /**
* Information for an advertised service including the service specific identity information. * Information for an advertised service including the service specific identity information.
@ -18,16 +17,17 @@ data class ServiceEntry(val info: ServiceInfo, val identity: PartyAndCertificate
/** /**
* Info about a network node that acts on behalf of some form of contract party. * Info about a network node that acts on behalf of some form of contract party.
*/ */
// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures.
@CordaSerializable @CordaSerializable
data class NodeInfo(val address: SingleMessageRecipient, data class NodeInfo(val addresses: List<NetworkHostAndPort>,
val legalIdentityAndCert: PartyAndCertificate, val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
val legalIdentitiesAndCerts: Set<PartyAndCertificate>,
val platformVersion: Int, val platformVersion: Int,
var advertisedServices: List<ServiceEntry> = emptyList(), var advertisedServices: List<ServiceEntry> = emptyList(),
val physicalLocation: PhysicalLocation? = null) { val worldMapLocation: WorldMapLocation? = null) {
init { init {
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" } require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
} }
val legalIdentity: Party val legalIdentity: Party
get() = legalIdentityAndCert.party get() = legalIdentityAndCert.party
val notaryIdentity: Party val notaryIdentity: Party
@ -35,7 +35,4 @@ data class NodeInfo(val address: SingleMessageRecipient,
fun serviceIdentities(type: ServiceType): List<Party> { fun serviceIdentities(type: ServiceType): List<Party> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party } return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
} }
fun servideIdentitiesAndCert(type: ServiceType): List<PartyAndCertificate> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
}
} }

View File

@ -43,15 +43,15 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) {
* The [countryCode] field is a two letter ISO country code. * The [countryCode] field is a two letter ISO country code.
*/ */
@CordaSerializable @CordaSerializable
data class PhysicalLocation(val coordinate: WorldCoordinate, val description: String, val countryCode: String) data class WorldMapLocation(val coordinate: WorldCoordinate, val description: String, val countryCode: String)
/** /**
* A simple lookup table of city names to their coordinates. Lookups are case insensitive. * A simple lookup table of city names to their coordinates. Lookups are case insensitive.
*/ */
object CityDatabase { object CityDatabase {
private val matcher = Regex("^([a-zA-Z- ]*) \\((..)\\)$") private val matcher = Regex("^([a-zA-Z- ]*) \\((..)\\)$")
private val caseInsensitiveLookups = HashMap<String, PhysicalLocation>() private val caseInsensitiveLookups = HashMap<String, WorldMapLocation>()
val cityMap = HashMap<String, PhysicalLocation>() val cityMap = HashMap<String, WorldMapLocation>()
init { init {
javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines -> javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines ->
@ -60,7 +60,7 @@ object CityDatabase {
val (name, lng, lat) = line.split('\t') val (name, lng, lat) = line.split('\t')
val matchResult = matcher.matchEntire(name) ?: throw Exception("Could not parse line: $line") val matchResult = matcher.matchEntire(name) ?: throw Exception("Could not parse line: $line")
val (city, country) = matchResult.destructured val (city, country) = matchResult.destructured
val location = PhysicalLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), city, country) val location = WorldMapLocation(WorldCoordinate(lat.toDouble(), lng.toDouble()), city, country)
caseInsensitiveLookups[city.toLowerCase()] = location caseInsensitiveLookups[city.toLowerCase()] = location
cityMap[city] = location cityMap[city] = location
} }

View File

@ -1,5 +1,6 @@
package net.corda.core.node package net.corda.core.node
import com.google.common.collect.Lists
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.node.services.* import net.corda.core.node.services.*
@ -17,7 +18,9 @@ import java.time.Clock
*/ */
interface ServicesForResolution { interface ServicesForResolution {
val identityService: IdentityService val identityService: IdentityService
val storageService: AttachmentsStorageService
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
val attachments: AttachmentStorage
/** /**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
@ -40,7 +43,14 @@ interface ServiceHub : ServicesForResolution {
val vaultService: VaultService val vaultService: VaultService
val vaultQueryService: VaultQueryService val vaultQueryService: VaultQueryService
val keyManagementService: KeyManagementService val keyManagementService: KeyManagementService
override val storageService: StorageService
/**
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.
* The signatures aren't technically needed after that point, but we keep them around so that we can relay
* the transaction data to other nodes that need it.
*/
val validatedTransactions: TransactionStorage
val networkMapCache: NetworkMapCache val networkMapCache: NetworkMapCache
val transactionVerifierService: TransactionVerifierService val transactionVerifierService: TransactionVerifierService
val clock: Clock val clock: Clock
@ -54,41 +64,41 @@ interface ServiceHub : ServicesForResolution {
fun <T : SerializeAsToken> cordaService(type: Class<T>): T fun <T : SerializeAsToken> cordaService(type: Class<T>): T
/** /**
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* sends them to the vault for further processing. Expects to be run within a database transaction. * further processing. This is expected to be run within a database transaction.
* *
* @param txs The transactions to record. * @param txs The transactions to record.
*/ */
// TODO: Make this take a single tx.
fun recordTransactions(txs: Iterable<SignedTransaction>) fun recordTransactions(txs: Iterable<SignedTransaction>)
/** /**
* Given some [SignedTransaction]s, writes them to the local storage for validated transactions and then * Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* sends them to the vault for further processing. * further processing. This is expected to be run within a database transaction.
*
* @param txs The transactions to record.
*/ */
fun recordTransactions(vararg txs: SignedTransaction) = recordTransactions(txs.toList()) fun recordTransactions(first: SignedTransaction, vararg remaining: SignedTransaction) {
recordTransactions(Lists.asList(first, remaining))
}
/** /**
* Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState]. * Given a [StateRef] loads the referenced transaction and looks up the specified output [ContractState].
* *
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction. * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/ */
@Throws(TransactionResolutionException::class) @Throws(TransactionResolutionException::class)
override fun loadState(stateRef: StateRef): TransactionState<*> { override fun loadState(stateRef: StateRef): TransactionState<*> {
val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash) val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return definingTx.tx.outputs[stateRef.index] return stx.tx.outputs[stateRef.index]
} }
/** /**
* Will check [logicType] and [args] against a whitelist and if acceptable then construct and initiate the protocol. * Converts the given [StateRef] into a [StateAndRef] object.
* *
* @throws IllegalProtocolLogicException or IllegalArgumentException if there are problems with the [logicType] or [args]. * @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/ */
fun <T : ContractState> toStateAndRef(ref: StateRef): StateAndRef<T> { @Throws(TransactionResolutionException::class)
val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash) fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
return definingTx.tx.outRef<T>(ref.index) val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return stx.tx.outRef<T>(stateRef.index)
} }
/** /**
@ -96,7 +106,7 @@ interface ServiceHub : ServicesForResolution {
* Node's primary signing identity. * Node's primary signing identity.
* Typical use is during signing in flows and for unit test signing. * Typical use is during signing in flows and for unit test signing.
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
* the matching [PrivateKey] will be looked up internally and used to sign. * the matching [java.security.PrivateKey] will be looked up internally and used to sign.
* If the key is actually a CompositeKey, the first leaf key hosted on this node * If the key is actually a CompositeKey, the first leaf key hosted on this node
* will be used to create the signature. * will be used to create the signature.
*/ */
@ -108,8 +118,8 @@ interface ServiceHub : ServicesForResolution {
* otherwise an IllegalArgumentException will be thrown. * otherwise an IllegalArgumentException will be thrown.
* Typical use is during signing in flows and for unit test signing. * Typical use is during signing in flows and for unit test signing.
* When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService * When this [PublicKey] is passed into the signing methods below, or on the KeyManagementService
* the matching [PrivateKey] will be looked up internally and used to sign. * the matching [java.security.PrivateKey] will be looked up internally and used to sign.
* If the key is actually a [CompositeKey], the first leaf key hosted on this node * If the key is actually a [net.corda.core.crypto.CompositeKey], the first leaf key hosted on this node
* will be used to create the signature. * will be used to create the signature.
*/ */
val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey
@ -119,7 +129,7 @@ interface ServiceHub : ServicesForResolution {
* using keys stored inside the node. * using keys stored inside the node.
* @param builder The [TransactionBuilder] to seal with the node's signature. * @param builder The [TransactionBuilder] to seal with the node's signature.
* Any existing signatures on the builder will be preserved. * Any existing signatures on the builder will be preserved.
* @param publicKey The [PublicKey] matched to the internal [PrivateKey] to use in signing this transaction. * @param publicKey The [PublicKey] matched to the internal [java.security.PrivateKey] to use in signing this transaction.
* If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node * If the passed in key is actually a CompositeKey the code searches for the first child key hosted within this node
* to sign with. * to sign with.
* @return Returns a SignedTransaction with the new node signature attached. * @return Returns a SignedTransaction with the new node signature attached.
@ -130,7 +140,6 @@ interface ServiceHub : ServicesForResolution {
return builder.toSignedTransaction(false) return builder.toSignedTransaction(false)
} }
/** /**
* Helper method to construct an initial partially signed transaction from a TransactionBuilder * Helper method to construct an initial partially signed transaction from a TransactionBuilder
* using the default identity key contained in the node. * using the default identity key contained in the node.
@ -140,36 +149,35 @@ interface ServiceHub : ServicesForResolution {
*/ */
fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey) fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey)
/** /**
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder] * Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
* using a set of keys all held in this node. * using a set of keys all held in this node.
* @param builder The [TransactionBuilder] to seal with the node's signature. * @param builder The [TransactionBuilder] to seal with the node's signature.
* Any existing signatures on the builder will be preserved. * Any existing signatures on the builder will be preserved.
* @param signingPubKeys A list of [PublicKeys] used to lookup the matching [PrivateKey] and sign. * @param signingPubKeys A list of [PublicKey]s used to lookup the matching [java.security.PrivateKey] and sign.
* @throws IllegalArgumentException is thrown if any keys are unavailable locally. * @throws IllegalArgumentException is thrown if any keys are unavailable locally.
* @return Returns a [SignedTransaction] with the new node signature attached. * @return Returns a [SignedTransaction] with the new node signature attached.
*/ */
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable<PublicKey>): SignedTransaction { fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable<PublicKey>): SignedTransaction {
var stx: SignedTransaction? = null val it = signingPubKeys.iterator()
for (pubKey in signingPubKeys) { var stx = signInitialTransaction(builder, it.next())
stx = if (stx == null) { while (it.hasNext()) {
signInitialTransaction(builder, pubKey) stx = addSignature(stx, it.next())
} else {
addSignature(stx, pubKey)
}
} }
return stx!! return stx
} }
/** /**
* Helper method to create an additional signature for an existing (partially) [SignedTransaction]. * Helper method to create an additional signature for an existing (partially) [SignedTransaction].
* @param signedTransaction The [SignedTransaction] to which the signature will apply. * @param signedTransaction The [SignedTransaction] to which the signature will apply.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. * @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. * If the [PublicKey] is actually a [net.corda.core.crypto.CompositeKey] the first leaf key found locally will be used
* @return The [DigitalSignature.WithKey] generated by signing with the internally held [PrivateKey]. * for signing.
* @return The [DigitalSignature.WithKey] generated by signing with the internally held [java.security.PrivateKey].
*/ */
fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey = keyManagementService.sign(signedTransaction.id.bytes, publicKey) fun createSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): DigitalSignature.WithKey {
return keyManagementService.sign(signedTransaction.id.bytes, publicKey)
}
/** /**
* Helper method to create an additional signature for an existing (partially) SignedTransaction * Helper method to create an additional signature for an existing (partially) SignedTransaction
@ -177,16 +185,21 @@ interface ServiceHub : ServicesForResolution {
* @param signedTransaction The SignedTransaction to which the signature will apply. * @param signedTransaction The SignedTransaction to which the signature will apply.
* @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey. * @return The DigitalSignature.WithKey generated by signing with the internally held identity PrivateKey.
*/ */
fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey = createSignature(signedTransaction, legalIdentityKey) fun createSignature(signedTransaction: SignedTransaction): DigitalSignature.WithKey {
return createSignature(signedTransaction, legalIdentityKey)
}
/** /**
* Helper method to append an additional signature to an existing (partially) [SignedTransaction]. * Helper method to append an additional signature to an existing (partially) [SignedTransaction].
* @param signedTransaction The [SignedTransaction] to which the signature will be added. * @param signedTransaction The [SignedTransaction] to which the signature will be added.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node. * @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing. * If the [PublicKey] is actually a [net.corda.core.crypto.CompositeKey] the first leaf key found locally will be used
* for signing.
* @return A new [SignedTransaction] with the addition of the new signature. * @return A new [SignedTransaction] with the addition of the new signature.
*/ */
fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction = signedTransaction + createSignature(signedTransaction, publicKey) fun addSignature(signedTransaction: SignedTransaction, publicKey: PublicKey): SignedTransaction {
return signedTransaction + createSignature(signedTransaction, publicKey)
}
/** /**
* Helper method to ap-pend an additional signature for an existing (partially) [SignedTransaction] * Helper method to ap-pend an additional signature for an existing (partially) [SignedTransaction]

View File

@ -2,21 +2,14 @@ package net.corda.core.node.services
import net.corda.core.contracts.Attachment import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.nio.file.Path import java.nio.file.FileAlreadyExistsException
/** /**
* An attachment store records potentially large binary objects, identified by their hash. * An attachment store records potentially large binary objects, identified by their hash.
*/ */
interface AttachmentStorage { interface AttachmentStorage {
/**
* If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for
* human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory
* will not have any effect).
*/
var automaticallyExtractAttachments: Boolean
var storePath: Path
/** /**
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open * Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
* a stream for the data, which will be a zip/jar file. * a stream for the data, which will be a zip/jar file.
@ -27,13 +20,14 @@ interface AttachmentStorage {
* Inserts the given attachment into the store, does *not* close the input stream. This can be an intensive * Inserts the given attachment into the store, does *not* close the input stream. This can be an intensive
* operation due to the need to copy the bytes to disk and hash them along the way. * operation due to the need to copy the bytes to disk and hash them along the way.
* *
* Note that you should not pass a [JarInputStream] into this method and it will throw if you do, because access * Note that you should not pass a [java.util.jar.JarInputStream] into this method and it will throw if you do, because
* to the raw byte stream is required. * access to the raw byte stream is required.
* *
* @throws FileAlreadyExistsException if the given byte stream has already been inserted. * @throws FileAlreadyExistsException if the given byte stream has already been inserted.
* @throws IllegalArgumentException if the given byte stream is empty or a [JarInputStream]. * @throws IllegalArgumentException if the given byte stream is empty or a [java.util.jar.JarInputStream].
* @throws IOException if something went wrong. * @throws IOException if something went wrong.
*/ */
@Throws(FileAlreadyExistsException::class, IOException::class)
fun importAttachment(jar: InputStream): SecureHash fun importAttachment(jar: InputStream): SecureHash
} }

View File

@ -2,8 +2,11 @@ package net.corda.core.node.services
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.Contract import net.corda.core.contracts.Contract
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.randomOrNull import net.corda.core.randomOrNull
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
@ -48,7 +51,7 @@ interface NetworkMapCache {
* Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the * Atomically get the current party nodes and a stream of updates. Note that the Observable buffers updates until the
* first subscriber is registered so as to avoid racing with early updates. * first subscriber is registered so as to avoid racing with early updates.
*/ */
fun track(): Pair<List<NodeInfo>, Observable<MapChange>> fun track(): DataFeed<List<NodeInfo>, MapChange>
/** Get the collection of nodes which advertise a specific service. */ /** Get the collection of nodes which advertise a specific service. */
fun getNodesWithService(serviceType: ServiceType): List<NodeInfo> { fun getNodesWithService(serviceType: ServiceType): List<NodeInfo> {
@ -62,6 +65,17 @@ interface NetworkMapCache {
*/ */
fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull() fun getRecommended(type: ServiceType, contract: Contract, vararg party: Party): NodeInfo? = getNodesWithService(type).firstOrNull()
/**
* Look up the node info for a specific party. Will attempt to de-anonymise the party if applicable; if the party
* is anonymised and the well known party cannot be resolved, it is impossible ot identify the node and therefore this
* returns null.
*
* @param party party to retrieve node information for.
* @return the node for the identity, or null if the node could not be found. This does not necessarily mean there is
* no node for the party, only that this cache is unaware of it.
*/
fun getNodeByLegalIdentity(party: AbstractParty): NodeInfo?
/** Look up the node info for a legal name. */ /** Look up the node info for a legal name. */
fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal } fun getNodeByLegalName(principal: X500Name): NodeInfo? = partyNodes.singleOrNull { it.legalIdentity.name == principal }

View File

@ -0,0 +1,78 @@
package net.corda.core.node.services
import net.corda.core.contracts.StateRef
import net.corda.core.contracts.TimeWindow
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.SignedData
import net.corda.core.flows.FlowLogic
import net.corda.core.identity.Party
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.SingletonSerializeAsToken
import net.corda.core.serialization.serialize
import net.corda.core.utilities.loggerFor
import net.corda.flows.NotaryError
import net.corda.flows.NotaryException
import org.slf4j.Logger
abstract class NotaryService : SingletonSerializeAsToken() {
abstract val services: ServiceHub
abstract fun start()
abstract fun stop()
/**
* Produces a notary service flow which has the corresponding sends and receives as [NotaryFlow.Client].
* The first parameter is the client [Party] making the request and the second is the platform version
* of the client's node. Use this version parameter to provide backwards compatibility if the notary flow protocol
* changes.
*/
abstract fun createServiceFlow(otherParty: Party, platformVersion: Int): FlowLogic<Void?>
}
/**
* A base notary service implementation that provides functionality for cases where a signature by a single member
* of the cluster is sufficient for transaction notarisation. For example, a single-node or a Raft notary.
*/
abstract class TrustedAuthorityNotaryService : NotaryService() {
protected open val log: Logger = loggerFor<TrustedAuthorityNotaryService>()
// TODO: specify the valid time window in config, and convert TimeWindowChecker to a utility method
protected abstract val timeWindowChecker: TimeWindowChecker
protected abstract val uniquenessProvider: UniquenessProvider
fun validateTimeWindow(t: TimeWindow?) {
if (t != null && !timeWindowChecker.isValid(t))
throw NotaryException(NotaryError.TimeWindowInvalid)
}
/**
* 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.
*/
fun commitInputStates(inputs: List<StateRef>, txId: SecureHash, caller: Party) {
try {
uniquenessProvider.commit(inputs, txId, caller)
} catch (e: UniquenessException) {
val conflicts = inputs.filterIndexed { i, stateRef ->
val consumingTx = e.error.stateHistory[stateRef]
consumingTx != null && consumingTx != UniquenessProvider.ConsumingTx(txId, i, caller)
}
if (conflicts.isNotEmpty()) {
// TODO: Create a new UniquenessException that only contains the conflicts filtered above.
log.warn("Notary conflicts for $txId: $conflicts")
throw notaryException(txId, e)
}
}
}
private fun notaryException(txId: SecureHash, e: UniquenessException): NotaryException {
val conflictData = e.error.serialize()
val signedConflict = SignedData(conflictData, sign(conflictData.bytes))
return NotaryException(NotaryError.Conflict(txId, signedConflict))
}
fun sign(bits: ByteArray): DigitalSignature.WithKey {
return services.keyManagementService.sign(bits, services.notaryIdentityKey)
}
}

View File

@ -3,8 +3,7 @@ package net.corda.core.node.services
import co.paralleluniverse.fibers.Suspendable import co.paralleluniverse.fibers.Suspendable
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.node.services.vault.QueryCriteria import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.keys import net.corda.core.crypto.keys
@ -12,20 +11,22 @@ import net.corda.core.flows.FlowException
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.identity.PartyAndCertificate import net.corda.core.identity.PartyAndCertificate
import net.corda.core.messaging.DataFeed
import net.corda.core.node.services.vault.PageSpecification import net.corda.core.node.services.vault.PageSpecification
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.node.services.vault.Sort import net.corda.core.node.services.vault.Sort
import net.corda.core.node.services.vault.DEFAULT_PAGE_SIZE
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import net.corda.core.toFuture import net.corda.core.toFuture
import net.corda.core.transactions.LedgerTransaction import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import org.bouncycastle.cert.X509CertificateHolder import net.corda.flows.AnonymisedIdentity
import rx.Observable import rx.Observable
import rx.subjects.PublishSubject import rx.subjects.PublishSubject
import java.io.InputStream import java.io.InputStream
import java.security.PublicKey import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -71,9 +72,9 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
/** Checks whether the update contains a state of the specified type and state status */ /** Checks whether the update contains a state of the specified type and state status */
fun <T : ContractState> containsType(clazz: Class<T>, status: StateStatus) = fun <T : ContractState> containsType(clazz: Class<T>, status: StateStatus) =
when(status) { when (status) {
StateStatus.UNCONSUMED -> produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } StateStatus.UNCONSUMED -> produced.any { clazz.isAssignableFrom(it.state.data.javaClass) }
StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } StateStatus.CONSUMED -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) }
else -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) } else -> consumed.any { clazz.isAssignableFrom(it.state.data.javaClass) }
|| produced.any { clazz.isAssignableFrom(it.state.data.javaClass) } || produced.any { clazz.isAssignableFrom(it.state.data.javaClass) }
} }
@ -118,17 +119,20 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
* A Page contains: * A Page contains:
* 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE] * 1) a [List] of actual [StateAndRef] requested by the specified [QueryCriteria] to a maximum of [MAX_PAGE_SIZE]
* 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result * 2) a [List] of associated [Vault.StateMetadata], one per [StateAndRef] result
* 3) the [PageSpecification] definition used to bound this result set * 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided
* 4) a total number of states that met the given [QueryCriteria] * (otherwise defaults to -1)
* Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform * 4) Status types used in this query: UNCONSUMED, CONSUMED, ALL
* further pagination (by issuing new queries). * 5) Other results as a [List] of any type (eg. aggregate function results with/without group by)
*
* Note: currently otherResults are used only for Aggregate Functions (in which case, the states and statesMetadata
* results will be empty)
*/ */
@CordaSerializable @CordaSerializable
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>, data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
val statesMetadata: List<StateMetadata>, val statesMetadata: List<StateMetadata>,
val pageable: PageSpecification, val totalStatesAvailable: Long,
val totalStatesAvailable: Int, val stateTypes: StateStatus,
val stateTypes: StateStatus) val otherResults: List<Any>)
@CordaSerializable @CordaSerializable
data class StateMetadata(val ref: StateRef, data class StateMetadata(val ref: StateRef,
@ -140,9 +144,6 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val notaryKey: String, val notaryKey: String,
val lockId: String?, val lockId: String?,
val lockUpdateTime: Instant?) val lockUpdateTime: Instant?)
@CordaSerializable
data class PageAndUpdates<out T : ContractState> (val current: Vault.Page<T>, val future: Observable<Vault.Update>)
} }
/** /**
@ -189,7 +190,7 @@ interface VaultService {
*/ */
// TODO: Remove this from the interface // TODO: Remove this from the interface
@Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())")) @Deprecated("This function will be removed in a future milestone", ReplaceWith("trackBy(QueryCriteria())"))
fun track(): Pair<Vault<ContractState>, Observable<Vault.Update>> fun track(): DataFeed<Vault<ContractState>, Vault.Update>
/** /**
* Return unconsumed [ContractState]s for a given set of [StateRef]s * Return unconsumed [ContractState]s for a given set of [StateRef]s
@ -274,7 +275,7 @@ interface VaultService {
* Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true) * Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true)
*/ */
// TODO: Remove this from the interface // TODO: Remove this from the interface
@Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())")) @Deprecated("This function will be removed in a future milestone", ReplaceWith("queryBy(QueryCriteria())"))
fun <T : ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>, includeSoftLockedStates: Boolean = true): Iterable<StateAndRef<T>> fun <T : ContractState> states(clazzes: Set<Class<T>>, statuses: EnumSet<Vault.StateStatus>, includeSoftLockedStates: Boolean = true): Iterable<StateAndRef<T>>
// DOCEND VaultStatesQuery // DOCEND VaultStatesQuery
@ -350,15 +351,18 @@ interface VaultQueryService {
* and returns a [Vault.Page] object containing the following: * and returns a [Vault.Page] object containing the following:
* 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification]) * 1. states as a List of <StateAndRef> (page number and size defined by [PageSpecification])
* 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table. * 2. states metadata as a List of [Vault.StateMetadata] held in the Vault States table.
* 3. the [PageSpecification] used in the query * 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
* 4. a total number of results available (for subsequent paging if necessary) * 4. status types used in this query: UNCONSUMED, CONSUMED, ALL
* 5. other results (aggregate functions with/without using value groups)
* *
* @throws VaultQueryException if the query cannot be executed for any reason * @throws VaultQueryException if the query cannot be executed for any reason
* (missing criteria or parsing error, invalid operator, unsupported query, underlying database error) * (missing criteria or parsing error, paging errors, unsupported query, underlying database error)
* *
* Note: a default [PageSpecification] is applied to the query returning the 1st page (indexed from 0) with up to 200 entries. * Notes
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification]. * If no [PageSpecification] is provided, a maximum of [DEFAULT_PAGE_SIZE] results will be returned.
* Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting * API users must specify a [PageSpecification] if they are expecting more than [DEFAULT_PAGE_SIZE] results,
* otherwise a [VaultQueryException] will be thrown alerting to this condition.
* It is the responsibility of the API user to request further pages and/or specify a more suitable [PageSpecification].
*/ */
@Throws(VaultQueryException::class) @Throws(VaultQueryException::class)
fun <T : ContractState> _queryBy(criteria: QueryCriteria, fun <T : ContractState> _queryBy(criteria: QueryCriteria,
@ -381,7 +385,7 @@ interface VaultQueryService {
fun <T : ContractState> _trackBy(criteria: QueryCriteria, fun <T : ContractState> _trackBy(criteria: QueryCriteria,
paging: PageSpecification, paging: PageSpecification,
sorting: Sort, sorting: Sort,
contractType: Class<out T>): Vault.PageAndUpdates<T> contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update>
// DOCEND VaultQueryAPI // DOCEND VaultQueryAPI
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations // Note: cannot apply @JvmOverloads to interfaces nor interface implementations
@ -402,19 +406,19 @@ interface VaultQueryService {
return _queryBy(criteria, paging, sorting, contractType) return _queryBy(criteria, paging, sorting, contractType)
} }
fun <T : ContractState> trackBy(contractType: Class<out T>): Vault.Page<T> { fun <T : ContractState> trackBy(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update> {
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType) return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
} }
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria): Vault.PageAndUpdates<T> { fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType) return _trackBy(criteria, PageSpecification(), Sort(emptySet()), contractType)
} }
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> { fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, paging, Sort(emptySet()), contractType) return _trackBy(criteria, paging, Sort(emptySet()), contractType)
} }
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> { fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, PageSpecification(), sorting, contractType) return _trackBy(criteria, PageSpecification(), sorting, contractType)
} }
fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates<T> { fun <T : ContractState> trackBy(contractType: Class<out T>, criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, paging, sorting, contractType) return _trackBy(criteria, paging, sorting, contractType)
} }
} }
@ -439,23 +443,23 @@ inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: Query
return _queryBy(criteria, paging, sorting, T::class.java) return _queryBy(criteria, paging, sorting, T::class.java)
} }
inline fun <reified T : ContractState> VaultQueryService.trackBy(): Vault.PageAndUpdates<T> { inline fun <reified T : ContractState> VaultQueryService.trackBy(): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java) return _trackBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), T::class.java)
} }
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria): Vault.PageAndUpdates<T> { inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java) return _trackBy(criteria, PageSpecification(), Sort(emptySet()), T::class.java)
} }
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): Vault.PageAndUpdates<T> { inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, paging, Sort(emptySet()), T::class.java) return _trackBy(criteria, paging, Sort(emptySet()), T::class.java)
} }
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): Vault.PageAndUpdates<T> { inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, PageSpecification(), sorting, T::class.java) return _trackBy(criteria, PageSpecification(), sorting, T::class.java)
} }
inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): Vault.PageAndUpdates<T> { inline fun <reified T : ContractState> VaultQueryService.trackBy(criteria: QueryCriteria, paging: PageSpecification, sorting: Sort): DataFeed<Vault.Page<T>, Vault.Update> {
return _trackBy(criteria, paging, sorting, T::class.java) return _trackBy(criteria, paging, sorting, T::class.java)
} }
@ -488,9 +492,17 @@ interface KeyManagementService {
* @return X.509 certificate and path to the trust root. * @return X.509 certificate and path to the trust root.
*/ */
@Suspendable @Suspendable
fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): Pair<X509CertificateHolder, CertPath> fun freshKeyAndCert(identity: PartyAndCertificate, revocationEnabled: Boolean): AnonymisedIdentity
/** Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data. /**
* Filter some keys down to the set that this node owns (has private keys for).
*
* @param candidateKeys keys which this node may own.
*/
fun filterMyKeys(candidateKeys: Iterable<PublicKey>): Iterable<PublicKey>
/**
* Using the provided signing [PublicKey] internally looks up the matching [PrivateKey] and signs the data.
* @param bytes The data to sign over using the chosen key. * @param bytes The data to sign over using the chosen key.
* @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity, * @param publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
* or previously generated via the [freshKey] method. * or previously generated via the [freshKey] method.
@ -521,44 +533,6 @@ interface FileUploader {
fun accepts(type: String): Boolean fun accepts(type: String): Boolean
} }
interface AttachmentsStorageService {
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
val attachments: AttachmentStorage
val attachmentsClassLoaderEnabled: Boolean
}
/**
* 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 : 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
* the transaction data to other nodes that need it.
*/
val validatedTransactions: ReadOnlyTransactionStorage
@Suppress("DEPRECATION")
@Deprecated("This service will be removed in a future milestone")
val uploaders: List<FileUploader>
val stateMachineRecordedTransactionMapping: StateMachineRecordedTransactionMappingStorage
}
/**
* Storage service, with extensions to allow validated transactions to be added to. For use only within [ServiceHub].
*/
interface TxWritableStorageService : StorageService {
/**
* A map of hash->tx where tx has been signature/contract validated and the states are known to be correct.
* The signatures aren't technically needed after that point, but we keep them around so that we can relay
* the transaction data to other nodes that need it.
*/
override val validatedTransactions: TransactionStorage
}
/** /**
* Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC. * Provides verification service. The implementation may be a simple in-memory verify() call or perhaps an IPC/RPC.
*/ */

View File

@ -1,18 +0,0 @@
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)
/**
* This is the interface to storage storing state machine -> recorded tx mappings. Any time a transaction is recorded
* during a flow run [addMapping] should be called.
*/
interface StateMachineRecordedTransactionMappingStorage {
fun addMapping(stateMachineRunId: StateMachineRunId, transactionId: SecureHash)
fun track(): Pair<List<StateMachineTransactionMapping>, Observable<StateMachineTransactionMapping>>
}

View File

@ -1,13 +1,14 @@
package net.corda.core.node.services package net.corda.core.node.services
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
import rx.Observable import rx.Observable
/** /**
* Thread-safe storage of transactions. * Thread-safe storage of transactions.
*/ */
interface ReadOnlyTransactionStorage { interface TransactionStorage {
/** /**
* Return the transaction with the given [id], or null if no such transaction exists. * Return the transaction with the given [id], or null if no such transaction exists.
*/ */
@ -22,19 +23,5 @@ interface ReadOnlyTransactionStorage {
/** /**
* Returns all currently stored transactions and further fresh ones. * Returns all currently stored transactions and further fresh ones.
*/ */
fun track(): Pair<List<SignedTransaction>, Observable<SignedTransaction>> fun track(): DataFeed<List<SignedTransaction>, SignedTransaction>
} }
/**
* Thread-safe storage of transactions.
*/
interface TransactionStorage : ReadOnlyTransactionStorage {
/**
* Add a new transaction to the store. If the store already has a transaction with the same id it will be
* overwritten.
* @param transaction The transaction to be recorded.
* @return true if the transaction was recorded successfully, false if it was already recorded.
*/
// TODO: Throw an exception if trying to add a transaction with fewer signatures than an existing entry.
fun addTransaction(transaction: SignedTransaction): Boolean
}

View File

@ -1,3 +1,5 @@
@file:JvmName("QueryCriteria")
package net.corda.core.node.services.vault package net.corda.core.node.services.vault
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
@ -5,11 +7,9 @@ import net.corda.core.contracts.StateRef
import net.corda.core.contracts.UniqueIdentifier import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.node.services.Vault import net.corda.core.node.services.Vault
import net.corda.core.node.services.vault.QueryCriteria.AndComposition
import net.corda.core.node.services.vault.QueryCriteria.OrComposition
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -26,32 +26,36 @@ sealed class QueryCriteria {
@CordaSerializable @CordaSerializable
data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate<Instant>) data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate<Instant>)
/** abstract class CommonQueryCriteria : QueryCriteria() {
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates] abstract val status: Vault.StateStatus
*/
data class VaultQueryCriteria @JvmOverloads constructor (
val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
val contractStateTypes: Set<Class<out ContractState>>? = null,
val stateRefs: List<StateRef>? = null,
val notaryName: List<X500Name>? = null,
val includeSoftlockedStates: Boolean = true,
val timeCondition: TimeCondition? = null) : QueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this)
} }
} }
/**
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
*/
data class VaultQueryCriteria @JvmOverloads constructor (override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED,
val contractStateTypes: Set<Class<out ContractState>>? = null,
val stateRefs: List<StateRef>? = null,
val notaryName: List<X500Name>? = null,
val includeSoftlockedStates: Boolean = true,
val timeCondition: TimeCondition? = null) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
}
}
/** /**
* LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState] * LinearStateQueryCriteria: provides query by attributes defined in [VaultSchema.VaultLinearState]
*/ */
data class LinearStateQueryCriteria @JvmOverloads constructor( data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val participants: List<AbstractParty>? = null, val linearId: List<UniqueIdentifier>? = null,
val linearId: List<UniqueIdentifier>? = null, val dealRef: List<String>? = null,
val dealRef: List<String>? = null) : QueryCriteria() { override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
} }
} }
@ -62,15 +66,14 @@ sealed class QueryCriteria {
* [Currency] as used in [Cash] contract state * [Currency] as used in [Cash] contract state
* [Commodity] as used in [CommodityContract] state * [Commodity] as used in [CommodityContract] state
*/ */
data class FungibleAssetQueryCriteria @JvmOverloads constructor( data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val participants: List<AbstractParty>? = null, val owner: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null, val quantity: ColumnPredicate<Long>? = null,
val quantity: ColumnPredicate<Long>? = null, val issuerPartyName: List<AbstractParty>? = null,
val issuerPartyName: List<AbstractParty>? = null, val issuerRef: List<OpaqueBytes>? = null,
val issuerRef: List<OpaqueBytes>? = null) : QueryCriteria() { override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
} }
} }
@ -84,20 +87,22 @@ sealed class QueryCriteria {
* *
* Refer to [CommercialPaper.State] for a concrete example. * Refer to [CommercialPaper.State] for a concrete example.
*/ */
data class VaultCustomQueryCriteria<L : PersistentState>(val expression: CriteriaExpression<L, Boolean>) : QueryCriteria() { data class VaultCustomQueryCriteria<L : PersistentState> @JvmOverloads constructor
(val expression: CriteriaExpression<L, Boolean>,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseCriteria(this) return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
} }
} }
// enable composition of [QueryCriteria] // enable composition of [QueryCriteria]
data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { private data class AndComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseAnd(this.a, this.b) return parser.parseAnd(this.a, this.b)
} }
} }
data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() { private data class OrComposition(val a: QueryCriteria, val b: QueryCriteria): QueryCriteria() {
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> { override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
return parser.parseOr(this.a, this.b) return parser.parseOr(this.a, this.b)
} }
@ -109,9 +114,13 @@ sealed class QueryCriteria {
RECORDED, RECORDED,
CONSUMED CONSUMED
} }
infix fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
} }
interface IQueryCriteriaParser { interface IQueryCriteriaParser {
fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate> fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate> fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): Collection<Predicate>
fun <L: PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate> fun <L: PersistentState> parseCriteria(criteria: QueryCriteria.VaultCustomQueryCriteria<L>): Collection<Predicate>
@ -120,6 +129,3 @@ interface IQueryCriteriaParser {
fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate> fun parseAnd(left: QueryCriteria, right: QueryCriteria): Collection<Predicate>
fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection<Predicate> fun parse(criteria: QueryCriteria, sorting: Sort? = null) : Collection<Predicate>
} }
infix fun QueryCriteria.and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
infix fun QueryCriteria.or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)

View File

@ -1,3 +1,5 @@
@file:JvmName("QueryCriteriaUtils")
package net.corda.core.node.services.vault package net.corda.core.node.services.vault
import net.corda.core.schemas.PersistentState import net.corda.core.schemas.PersistentState
@ -44,11 +46,23 @@ enum class CollectionOperator {
NOT_IN NOT_IN
} }
@CordaSerializable
enum class AggregateFunctionType {
COUNT,
AVG,
MIN,
MAX,
SUM,
}
@CordaSerializable @CordaSerializable
sealed class CriteriaExpression<O, out T> { sealed class CriteriaExpression<O, out T> {
data class BinaryLogical<O>(val left: CriteriaExpression<O, Boolean>, val right: CriteriaExpression<O, Boolean>, val operator: BinaryLogicalOperator) : CriteriaExpression<O, Boolean>() data class BinaryLogical<O>(val left: CriteriaExpression<O, Boolean>, val right: CriteriaExpression<O, Boolean>, val operator: BinaryLogicalOperator) : CriteriaExpression<O, Boolean>()
data class Not<O>(val expression: CriteriaExpression<O, Boolean>) : CriteriaExpression<O, Boolean>() data class Not<O>(val expression: CriteriaExpression<O, Boolean>) : CriteriaExpression<O, Boolean>()
data class ColumnPredicateExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>) : CriteriaExpression<O, Boolean>() data class ColumnPredicateExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>) : CriteriaExpression<O, Boolean>()
data class AggregateFunctionExpression<O, C>(val column: Column<O, C>, val predicate: ColumnPredicate<C>,
val groupByColumns: List<Column<O, C>>?,
val orderBy: Sort.Direction?) : CriteriaExpression<O, Boolean>()
} }
@CordaSerializable @CordaSerializable
@ -65,6 +79,7 @@ sealed class ColumnPredicate<C> {
data class CollectionExpression<C>(val operator: CollectionOperator, val rightLiteral: Collection<C>) : ColumnPredicate<C>() data class CollectionExpression<C>(val operator: CollectionOperator, val rightLiteral: Collection<C>) : ColumnPredicate<C>()
data class Between<C : Comparable<C>>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate<C>() data class Between<C : Comparable<C>>(val rightFromLiteral: C, val rightToLiteral: C) : ColumnPredicate<C>()
data class NullExpression<C>(val operator: NullOperator) : ColumnPredicate<C>() data class NullExpression<C>(val operator: NullOperator) : ColumnPredicate<C>()
data class AggregateFunction<C>(val type: AggregateFunctionType) : ColumnPredicate<C>()
} }
fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O, R>): Class<O> { fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O, R>): Class<O> {
@ -72,9 +87,11 @@ fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O
is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left) is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left)
is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression) is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression)
is CriteriaExpression.ColumnPredicateExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column) is CriteriaExpression.ColumnPredicateExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column)
is CriteriaExpression.AggregateFunctionExpression<O, *> -> resolveEnclosingObjectFromColumn(expression.column)
} }
} }
@Suppress("UNCHECKED_CAST")
fun <O, C> resolveEnclosingObjectFromColumn(column: Column<O, C>): Class<O> { fun <O, C> resolveEnclosingObjectFromColumn(column: Column<O, C>): Class<O> {
return when (column) { return when (column) {
is Column.Java -> column.field.declaringClass as Class<O> is Column.Java -> column.field.declaringClass as Class<O>
@ -102,21 +119,25 @@ fun <O, C> getColumnName(column: Column<O, C>): String {
* paging and sorting capability: * paging and sorting capability:
* https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html * https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html
*/ */
val DEFAULT_PAGE_NUM = 0 const val DEFAULT_PAGE_NUM = 1
val DEFAULT_PAGE_SIZE = 200 const val DEFAULT_PAGE_SIZE = 200
/** /**
* Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations) * Note: use [PageSpecification] to correctly handle a number of bounded pages of a pre-configured page size.
* Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE].
*/ */
val MAX_PAGE_SIZE = 512 const val MAX_PAGE_SIZE = Int.MAX_VALUE
/** /**
* PageSpecification allows specification of a page number (starting from 0 as default) and page size (defaulting to * [PageSpecification] allows specification of a page number (starting from [DEFAULT_PAGE_NUM]) and page size
* [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE] * (defaulting to [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE])
* Note: we default the page number to [DEFAULT_PAGE_SIZE] to enable queries without requiring a page specification
* but enabling detection of large results sets that fall out of the [DEFAULT_PAGE_SIZE] requirement.
* [MAX_PAGE_SIZE] should be used with extreme caution as results may exceed your JVM memory footprint.
*/ */
@CordaSerializable @CordaSerializable
data class PageSpecification(val pageNumber: Int = DEFAULT_PAGE_NUM, val pageSize: Int = DEFAULT_PAGE_SIZE) data class PageSpecification(val pageNumber: Int = -1, val pageSize: Int = DEFAULT_PAGE_SIZE) {
val isDefault = (pageSize == DEFAULT_PAGE_SIZE && pageNumber == -1)
}
/** /**
* Sort allows specification of a set of entity attribute names and their associated directionality * Sort allows specification of a set of entity attribute names and their associated directionality
@ -133,24 +154,30 @@ data class Sort(val columns: Collection<SortColumn>) {
@CordaSerializable @CordaSerializable
interface Attribute interface Attribute
enum class VaultStateAttribute(val columnName: String) : Attribute { enum class CommonStateAttribute(val attributeParent: String, val attributeChild: String?) : Attribute {
STATE_REF("stateRef", null),
STATE_REF_TXN_ID("stateRef", "txId"),
STATE_REF_INDEX("stateRef", "index")
}
enum class VaultStateAttribute(val attributeName: String) : Attribute {
/** Vault States */ /** Vault States */
NOTARY_NAME("notaryName"), NOTARY_NAME("notaryName"),
CONTRACT_TYPE("contractStateClassName"), CONTRACT_TYPE("contractStateClassName"),
STATE_STATUS("stateStatus"), STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"), RECORDED_TIME("recordedTime"),
CONSUMED_TIME("consumedTime"), CONSUMED_TIME("consumedTime"),
LOCK_ID("lockId"), LOCK_ID("lockId")
} }
enum class LinearStateAttribute(val columnName: String) : Attribute { enum class LinearStateAttribute(val attributeName: String) : Attribute {
/** Vault Linear States */ /** Vault Linear States */
UUID("uuid"), UUID("uuid"),
EXTERNAL_ID("externalId"), EXTERNAL_ID("externalId"),
DEAL_REFERENCE("dealReference"), DEAL_REFERENCE("dealReference")
} }
enum class FungibleStateAttribute(val columnName: String) : Attribute { enum class FungibleStateAttribute(val attributeName: String) : Attribute {
/** Vault Fungible States */ /** Vault Fungible States */
QUANTITY("quantity"), QUANTITY("quantity"),
ISSUER_REF("issuerRef") ISSUER_REF("issuerRef")
@ -183,10 +210,15 @@ sealed class SortAttribute {
object Builder { object Builder {
fun <R : Comparable<R>> compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value) fun <R : Comparable<R>> compare(operator: BinaryComparisonOperator, value: R) = ColumnPredicate.BinaryComparison(operator, value)
fun <O, R> KProperty1<O, R?>.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate) fun <O, R> KProperty1<O, R?>.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Kotlin(this), predicate)
fun <R> Field.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Java<Any, R>(this), predicate) fun <R> Field.predicate(predicate: ColumnPredicate<R>) = CriteriaExpression.ColumnPredicateExpression(Column.Java<Any, R>(this), predicate)
fun <O, R> KProperty1<O, R?>.functionPredicate(predicate: ColumnPredicate<R>, groupByColumns: List<Column.Kotlin<O, R>>? = null, orderBy: Sort.Direction? = null)
= CriteriaExpression.AggregateFunctionExpression(Column.Kotlin(this), predicate, groupByColumns, orderBy)
fun <R> Field.functionPredicate(predicate: ColumnPredicate<R>, groupByColumns: List<Column.Java<Any, R>>? = null, orderBy: Sort.Direction? = null)
= CriteriaExpression.AggregateFunctionExpression(Column.Java<Any, R>(this), predicate, groupByColumns, orderBy)
fun <O, R : Comparable<R>> KProperty1<O, R?>.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) fun <O, R : Comparable<R>> KProperty1<O, R?>.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
fun <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value)) fun <R : Comparable<R>> Field.comparePredicate(operator: BinaryComparisonOperator, value: R) = predicate(compare(operator, value))
@ -200,15 +232,15 @@ object Builder {
fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) fun <O, R : Comparable<R>> KProperty1<O, R?>.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) fun <O, R : Comparable<R>> KProperty1<O, R?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)) @JvmStatic fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)) @JvmStatic fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value) @JvmStatic fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value) @JvmStatic fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value) @JvmStatic fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value) @JvmStatic fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to)) @JvmStatic fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection)) @JvmStatic fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.IN, collection))
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)) @JvmStatic fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value) fun <R> equal(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value)
fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value) fun <R> notEqual(value: R) = ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value)
@ -221,14 +253,45 @@ object Builder {
fun <R : Comparable<R>> notIn(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection) fun <R : Comparable<R>> notIn(collection: Collection<R>) = ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection)
fun <O> KProperty1<O, String?>.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) fun <O> KProperty1<O, String?>.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string)) @JvmStatic fun Field.like(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.LIKE, string))
fun <O> KProperty1<O, String?>.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) fun <O> KProperty1<O, String?>.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string)) @JvmStatic fun Field.notLike(string: String) = predicate(ColumnPredicate.Likeness(LikenessOperator.NOT_LIKE, string))
fun <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL)) fun <O, R> KProperty1<O, R?>.isNull() = predicate(ColumnPredicate.NullExpression(NullOperator.IS_NULL))
fun Field.isNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.IS_NULL)) @JvmStatic fun Field.isNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.IS_NULL))
fun <O, R> KProperty1<O, R?>.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL)) fun <O, R> KProperty1<O, R?>.notNull() = predicate(ColumnPredicate.NullExpression(NullOperator.NOT_NULL))
fun Field.notNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.NOT_NULL)) @JvmStatic fun Field.notNull() = predicate(ColumnPredicate.NullExpression<Any>(NullOperator.NOT_NULL))
/** aggregate functions */
fun <O, R> KProperty1<O, R?>.sum(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.SUM), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic @JvmOverloads
fun <R> Field.sum(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.SUM), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.count() = functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.COUNT))
@JvmStatic fun Field.count() = functionPredicate(ColumnPredicate.AggregateFunction<Any>(AggregateFunctionType.COUNT))
fun <O, R> KProperty1<O, R?>.avg(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.AVG), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.avg(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.AVG), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.min(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MIN), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.min(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.MIN), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
fun <O, R> KProperty1<O, R?>.max(groupByColumns: List<KProperty1<O, R>>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction(AggregateFunctionType.MAX), groupByColumns?.map { Column.Kotlin(it) }, orderBy)
@JvmStatic
@JvmOverloads
fun <R> Field.max(groupByColumns: List<Field>? = null, orderBy: Sort.Direction? = null) =
functionPredicate(ColumnPredicate.AggregateFunction<R>(AggregateFunctionType.MAX), groupByColumns?.map { Column.Java<Any,R>(it) }, orderBy)
} }
inline fun <A> builder(block: Builder.() -> A) = block(Builder) inline fun <A> builder(block: Builder.() -> A) = block(Builder)

View File

@ -1,11 +1,11 @@
package net.corda.node.services.vault.schemas.jpa package net.corda.core.schemas
import net.corda.core.contracts.ContractState
import net.corda.core.contracts.FungibleAsset
import net.corda.core.contracts.OwnableState
import net.corda.core.contracts.UniqueIdentifier import net.corda.core.contracts.UniqueIdentifier
import net.corda.core.crypto.toBase58String import net.corda.core.crypto.toBase58String
import net.corda.core.identity.AbstractParty import net.corda.core.identity.AbstractParty
import net.corda.core.schemas.MappedSchema
import net.corda.core.schemas.PersistentState
import net.corda.core.schemas.StatePersistable
import java.util.* import java.util.*
import javax.persistence.* import javax.persistence.*
@ -90,7 +90,7 @@ object CommonSchemaV1 : MappedSchema(schemaFamily = CommonSchema.javaClass, vers
@Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?) @Column(name = "party_key", length = 65535) // TODO What is the upper limit on size of CompositeKey?)
var key: String var key: String
) { ) {
constructor(party: net.corda.core.identity.AbstractParty) constructor(party: AbstractParty)
: this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String()) : this(0, party.nameOrNull()?.toString() ?: party.toString(), party.owningKey.toBase58String())
} }
} }

View File

@ -1,25 +0,0 @@
package net.corda.core.schemas
/**
* An object used to fully qualify the [DummyDealStateSchema] family name (i.e. independent of version).
*/
object DummyDealStateSchema
/**
* First version of a cash contract ORM schema that maps all fields of the [DummyDealState] contract state as it stood
* at the time of writing.
*/
object DummyDealStateSchemaV1 : net.corda.core.schemas.MappedSchema(schemaFamily = net.corda.core.schemas.DummyDealStateSchema.javaClass, version = 1, mappedTypes = listOf(net.corda.core.schemas.DummyDealStateSchemaV1.PersistentDummyDealState::class.java)) {
@javax.persistence.Entity
@javax.persistence.Table(name = "dummy_deal_states")
class PersistentDummyDealState(
@javax.persistence.Column(name = "deal_reference")
var dealReference: String,
/** parent attributes */
@javax.persistence.Transient
val uid: net.corda.core.contracts.UniqueIdentifier
) : net.corda.node.services.vault.schemas.jpa.CommonSchemaV1.LinearState(uid = uid)
}

View File

@ -3,7 +3,7 @@ package net.corda.core.schemas
import io.requery.Persistable import io.requery.Persistable
import net.corda.core.contracts.ContractState import net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef import net.corda.core.contracts.StateRef
import net.corda.core.serialization.toHexString import net.corda.core.utilities.toHexString
import java.io.Serializable import java.io.Serializable
import javax.persistence.Column import javax.persistence.Column
import javax.persistence.Embeddable import javax.persistence.Embeddable

View File

@ -1,6 +1,8 @@
package net.corda.core.serialization package net.corda.core.serialization
import com.esotericsoftware.kryo.* import com.esotericsoftware.kryo.*
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import com.esotericsoftware.kryo.util.DefaultClassResolver import com.esotericsoftware.kryo.util.DefaultClassResolver
import com.esotericsoftware.kryo.util.Util import com.esotericsoftware.kryo.util.Util
import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.AttachmentsClassLoader
@ -29,7 +31,11 @@ fun makeAllButBlacklistedClassResolver(): ClassResolver {
return CordaClassResolver(AllButBlacklisted) return CordaClassResolver(AllButBlacklisted)
} }
class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver() { /**
* @param amqpEnabled Setting this to true turns on experimental AMQP serialization for any class annotated with
* [CordaSerializable].
*/
class CordaClassResolver(val whitelist: ClassWhitelist, val amqpEnabled: Boolean = false) : DefaultClassResolver() {
/** Returns the registration for the specified class, or null if the class is not registered. */ /** Returns the registration for the specified class, or null if the class is not registered. */
override fun getRegistration(type: Class<*>): Registration? { override fun getRegistration(type: Class<*>): Registration? {
return super.getRegistration(type) ?: checkClass(type) return super.getRegistration(type) ?: checkClass(type)
@ -59,7 +65,7 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
return checkClass(type.superclass) return checkClass(type.superclass)
} }
// It's safe to have the Class already, since Kryo loads it with initialisation off. // It's safe to have the Class already, since Kryo loads it with initialisation off.
// If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw a NotSerializableException if input class is blacklisted. // If we use a whitelist with blacklisting capabilities, whitelist.hasListed(type) may throw an IllegalStateException if input class is blacklisted.
// Thus, blacklisting precedes annotation checking. // Thus, blacklisting precedes annotation checking.
if (!whitelist.hasListed(type) && !checkForAnnotation(type)) { if (!whitelist.hasListed(type) && !checkForAnnotation(type)) {
throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization") throw KryoException("Class ${Util.className(type)} is not annotated or on the whitelist, so cannot be used in serialization")
@ -68,16 +74,38 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
} }
override fun registerImplicit(type: Class<*>): Registration { 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 hasAnnotation = checkForAnnotation(type)
val references = kryo.references // If something is not annotated, or AMQP is disabled, we stay serializing with Kryo. This will typically be the
try { // case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures
kryo.references = true // are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph.
return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt())) if (!hasAnnotation || !amqpEnabled) {
} finally { val objectInstance = try {
kryo.references = references type.kotlin.objectInstance
} catch (t: Throwable) {
// objectInstance will throw if the type is something like a lambda
null
}
// 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
val serializer = if (objectInstance != null) KotlinObjectSerializer(objectInstance) else kryo.getDefaultSerializer(type)
return register(Registration(type, serializer, NAME.toInt()))
} finally {
kryo.references = references
}
} else {
// Build AMQP serializer
return register(Registration(type, KryoAMQPSerializer, NAME.toInt()))
} }
} }
// Trivial Serializer which simply returns the given instance which we already know is a Kotlin object
private class KotlinObjectSerializer(val objectInstance: Any) : Serializer<Any>() {
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any = objectInstance
override fun write(kryo: Kryo, output: Output, obj: Any) = Unit
}
// 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 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. // 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. // TODO: Later we can support annotations on attachment classes and spin up a proxy via bytecode that we know is harmless.
@ -85,13 +113,13 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
return (type.classLoader !is AttachmentsClassLoader) return (type.classLoader !is AttachmentsClassLoader)
&& !KryoSerializable::class.java.isAssignableFrom(type) && !KryoSerializable::class.java.isAssignableFrom(type)
&& !type.isAnnotationPresent(DefaultSerializer::class.java) && !type.isAnnotationPresent(DefaultSerializer::class.java)
&& (type.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(type)) && (type.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(type))
} }
// Recursively check interfaces for our annotation. // Recursively check interfaces for our annotation.
private fun hasAnnotationOnInterface(type: Class<*>): Boolean { private fun hasInheritedAnnotation(type: Class<*>): Boolean {
return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(it) } return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) }
|| (type.superclass != null && hasAnnotationOnInterface(type.superclass)) || (type.superclass != null && hasInheritedAnnotation(type.superclass))
} }
// Need to clear out class names from attachments. // Need to clear out class names from attachments.
@ -152,8 +180,6 @@ class GlobalTransientClassWhiteList(val delegate: ClassWhitelist) : MutableClass
/** /**
* This class is not currently used, but can be installed to log a large number of missing entries from the whitelist * 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. * and was used to track down the initial set.
*
* @suppress
*/ */
@Suppress("unused") @Suppress("unused")
class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist { class LoggingWhitelist(val delegate: ClassWhitelist, val global: Boolean = true) : MutableClassWhitelist {

View File

@ -8,7 +8,7 @@ import de.javakaffee.kryoserializers.ArraysAsListSerializer
import de.javakaffee.kryoserializers.BitSetSerializer import de.javakaffee.kryoserializers.BitSetSerializer
import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer import de.javakaffee.kryoserializers.UnmodifiableCollectionsSerializer
import de.javakaffee.kryoserializers.guava.* import de.javakaffee.kryoserializers.guava.*
import net.corda.core.crypto.CompositeKey import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.MetaData import net.corda.core.crypto.MetaData
import net.corda.core.node.CordaPluginRegistry import net.corda.core.node.CordaPluginRegistry
import net.corda.core.transactions.SignedTransaction import net.corda.core.transactions.SignedTransaction
@ -25,14 +25,16 @@ import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPrivateCrtKey
import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey import org.bouncycastle.jcajce.provider.asymmetric.rsa.BCRSAPublicKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey import org.bouncycastle.pqc.jcajce.provider.sphincs.BCSphincs256PublicKey
import org.objenesis.instantiator.ObjectInstantiator
import org.objenesis.strategy.InstantiatorStrategy
import org.objenesis.strategy.StdInstantiatorStrategy import org.objenesis.strategy.StdInstantiatorStrategy
import org.slf4j.Logger import org.slf4j.Logger
import sun.security.provider.certpath.X509CertPath import sun.security.provider.certpath.X509CertPath
import java.io.BufferedInputStream import java.io.BufferedInputStream
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.lang.reflect.Modifier.isPublic
import java.security.cert.CertPath import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.* import java.util.*
object DefaultKryoCustomizer { object DefaultKryoCustomizer {
@ -51,9 +53,7 @@ object DefaultKryoCustomizer {
// Take the safest route here and allow subclasses to have fields named the same as super classes. // Take the safest route here and allow subclasses to have fields named the same as super classes.
fieldSerializerConfig.cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.EXTENDED fieldSerializerConfig.cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.EXTENDED
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no instantiatorStrategy = CustomInstantiatorStrategy()
// no-arg constructor available.
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
register(Arrays.asList("").javaClass, ArraysAsListSerializer()) register(Arrays.asList("").javaClass, ArraysAsListSerializer())
register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class)) register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class))
@ -73,7 +73,7 @@ object DefaultKryoCustomizer {
noReferencesWithin<WireTransaction>() noReferencesWithin<WireTransaction>()
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer) register(sun.security.ec.ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer) register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer) register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
@ -86,9 +86,6 @@ object DefaultKryoCustomizer {
// This ensures a NonEmptySetSerializer is constructed with an initial value. // This ensures a NonEmptySetSerializer is constructed with an initial value.
register(NonEmptySet::class.java, NonEmptySetSerializer) 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>()) addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
register(MetaData::class.java, MetaDataSerializer) register(MetaData::class.java, MetaDataSerializer)
@ -113,9 +110,22 @@ object DefaultKryoCustomizer {
register(BCRSAPublicKey::class.java, PublicKeySerializer) register(BCRSAPublicKey::class.java, PublicKeySerializer)
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer) register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
register(BCSphincs256PublicKey::class.java, PublicKeySerializer) register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
val customization = KryoSerializationCustomization(this) val customization = KryoSerializationCustomization(this)
pluginRegistries.forEach { it.customizeSerialization(customization) } pluginRegistries.forEach { it.customizeSerialization(customization) }
} }
} }
private class CustomInstantiatorStrategy : InstantiatorStrategy {
private val fallbackStrategy = StdInstantiatorStrategy()
// Use this to allow construction of objects using a JVM backdoor that skips invoking the constructors, if there
// is no no-arg constructor available.
private val defaultStrategy = Kryo.DefaultInstantiatorStrategy(fallbackStrategy)
override fun <T> newInstantiatorOf(type: Class<T>): ObjectInstantiator<T> {
// However this doesn't work for non-public classes in the java. namespace
val strat = if (type.name.startsWith("java.") && !isPublic(type.modifiers)) fallbackStrategy else defaultStrategy
return strat.newInstantiatorOf(type)
}
}
} }

View File

@ -9,11 +9,13 @@ import com.esotericsoftware.kryo.util.MapReferenceResolver
import com.google.common.annotations.VisibleForTesting import com.google.common.annotations.VisibleForTesting
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.node.AttachmentsClassLoader import net.corda.core.node.AttachmentsClassLoader
import net.corda.core.transactions.WireTransaction import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.LazyPool import net.corda.core.utilities.LazyPool
import net.corda.core.utilities.SgxSupport import net.corda.core.utilities.SgxSupport
import net.corda.core.utilities.OpaqueBytes
import net.i2p.crypto.eddsa.EdDSAPrivateKey import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey import net.i2p.crypto.eddsa.EdDSAPublicKey
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec
@ -321,6 +323,9 @@ class MissingAttachmentsException(val ids: List<SecureHash>) : Exception()
/** A serialisation engine that knows how to deserialise code inside a sandbox */ /** A serialisation engine that knows how to deserialise code inside a sandbox */
@ThreadSafe @ThreadSafe
object WireTransactionSerializer : Serializer<WireTransaction>() { object WireTransactionSerializer : Serializer<WireTransaction>() {
@VisibleForTesting
internal val attachmentsClassLoaderEnabled = "attachments.class.loader.enabled"
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) { override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
kryo.writeClassAndObject(output, obj.inputs) kryo.writeClassAndObject(output, obj.inputs)
kryo.writeClassAndObject(output, obj.attachments) kryo.writeClassAndObject(output, obj.attachments)
@ -333,12 +338,12 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
} }
private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? { private fun attachmentsClassLoader(kryo: Kryo, attachmentHashes: List<SecureHash>): ClassLoader? {
kryo.context[attachmentsClassLoaderEnabled] as? Boolean ?: false || return null
val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one. val serializationContext = kryo.serializationContext() ?: return null // Some tests don't set one.
serializationContext.serviceHub.storageService.attachmentsClassLoaderEnabled || return null
val missing = ArrayList<SecureHash>() val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>() val attachments = ArrayList<Attachment>()
attachmentHashes.forEach { id -> attachmentHashes.forEach { id ->
serializationContext.serviceHub.storageService.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id } serializationContext.serviceHub.attachments.openAttachment(id)?.let { attachments += it } ?: run { missing += id }
} }
missing.isNotEmpty() && throw MissingAttachmentsException(missing) missing.isNotEmpty() && throw MissingAttachmentsException(missing)
return AttachmentsClassLoader(attachments) return AttachmentsClassLoader(attachments)
@ -391,6 +396,20 @@ object Ed25519PublicKeySerializer : Serializer<EdDSAPublicKey>() {
} }
} }
/** For serialising an ed25519 public key */
@ThreadSafe
object ECPublicKeyImplSerializer : Serializer<sun.security.ec.ECPublicKeyImpl>() {
override fun write(kryo: Kryo, output: Output, obj: sun.security.ec.ECPublicKeyImpl) {
output.writeBytesWithLength(obj.encoded)
}
override fun read(kryo: Kryo, input: Input, type: Class<sun.security.ec.ECPublicKeyImpl>): sun.security.ec.ECPublicKeyImpl {
val A = input.readBytesWithLength()
val der = sun.security.util.DerValue(A)
return sun.security.ec.ECPublicKeyImpl.parse(der) as sun.security.ec.ECPublicKeyImpl
}
}
// TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249. // TODO Implement standardized serialization of CompositeKeys. See JIRA issue: CORDA-249.
@ThreadSafe @ThreadSafe
object CompositeKeySerializer : Serializer<CompositeKey>() { object CompositeKeySerializer : Serializer<CompositeKey>() {
@ -449,19 +468,6 @@ inline fun <reified T> readListOfLength(kryo: Kryo, input: Input, minLen: Int =
return list return list
} }
/** Marker interface for kotlin object definitions so that they are deserialized as the singleton instance. */
interface DeserializeAsKotlinObjectDef
/** Serializer to deserialize kotlin object definitions marked with [DeserializeAsKotlinObjectDef]. */
object KotlinObjectSerializer : Serializer<DeserializeAsKotlinObjectDef>() {
override fun read(kryo: Kryo, input: Input, type: Class<DeserializeAsKotlinObjectDef>): DeserializeAsKotlinObjectDef {
// read the public static INSTANCE field that kotlin compiler generates.
return type.getField("INSTANCE").get(null) as DeserializeAsKotlinObjectDef
}
override fun write(kryo: Kryo, output: Output, obj: DeserializeAsKotlinObjectDef) {}
}
// No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors. // No ClassResolver only constructor. MapReferenceResolver is the default as used by Kryo in other constructors.
private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build() private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build()
private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build() private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build()
@ -519,7 +525,7 @@ inline fun <T : Any> Kryo.register(
return register( return register(
type.java, type.java,
object : Serializer<T>() { object : Serializer<T>() {
override fun read(kryo: Kryo, input: Input, type: Class<T>): T = read(kryo, input) override fun read(kryo: Kryo, input: Input, clazz: Class<T>): T = read(kryo, input)
override fun write(kryo: Kryo, output: Output, obj: T) = write(kryo, output, obj) override fun write(kryo: Kryo, output: Output, obj: T) = write(kryo, output, obj)
} }
) )
@ -625,7 +631,7 @@ object X500NameSerializer : Serializer<X500Name>() {
*/ */
@ThreadSafe @ThreadSafe
object CertPathSerializer : Serializer<CertPath>() { object CertPathSerializer : Serializer<CertPath>() {
val factory = CertificateFactory.getInstance("X.509") val factory: CertificateFactory = CertificateFactory.getInstance("X.509")
override fun read(kryo: Kryo, input: Input, type: Class<CertPath>): CertPath { override fun read(kryo: Kryo, input: Input, type: Class<CertPath>): CertPath {
return factory.generateCertPath(input) return factory.generateCertPath(input)
} }
@ -636,7 +642,7 @@ object CertPathSerializer : Serializer<CertPath>() {
} }
/** /**
* For serialising an [CX509CertificateHolder] in an X.500 standard format. * For serialising an [X509CertificateHolder] in an X.500 standard format.
*/ */
@ThreadSafe @ThreadSafe
object X509CertificateSerializer : Serializer<X509CertificateHolder>() { object X509CertificateSerializer : Serializer<X509CertificateHolder>() {

View File

@ -0,0 +1,51 @@
package net.corda.core.serialization
import com.esotericsoftware.kryo.Kryo
import com.esotericsoftware.kryo.Serializer
import com.esotericsoftware.kryo.io.Input
import com.esotericsoftware.kryo.io.Output
import net.corda.core.serialization.amqp.DeserializationInput
import net.corda.core.serialization.amqp.SerializationOutput
import net.corda.core.serialization.amqp.SerializerFactory
/**
* This [Kryo] custom [Serializer] switches the object graph of anything annotated with `@CordaSerializable`
* to using the AMQP serialization wire format, and simply writes that out as bytes to the wire.
*
* There is no need to write out the length, since this can be peeked out of the first few bytes of the stream.
*/
object KryoAMQPSerializer : Serializer<Any>() {
internal fun registerCustomSerializers(factory: SerializerFactory) {
factory.apply {
register(net.corda.core.serialization.amqp.custom.PublicKeySerializer)
register(net.corda.core.serialization.amqp.custom.ThrowableSerializer(this))
register(net.corda.core.serialization.amqp.custom.X500NameSerializer)
register(net.corda.core.serialization.amqp.custom.BigDecimalSerializer)
register(net.corda.core.serialization.amqp.custom.CurrencySerializer)
register(net.corda.core.serialization.amqp.custom.InstantSerializer(this))
}
}
// TODO: need to sort out the whitelist... we currently do not apply the whitelist attached to the [Kryo]
// instance to the factory. We need to do this before turning on AMQP serialization.
private val serializerFactory = SerializerFactory().apply {
registerCustomSerializers(this)
}
override fun write(kryo: Kryo, output: Output, obj: Any) {
val amqpOutput = SerializationOutput(serializerFactory)
val bytes = amqpOutput.serialize(obj).bytes
// No need to write out the size since it's encoded within the AMQP.
output.write(bytes)
}
override fun read(kryo: Kryo, input: Input, type: Class<Any>): Any {
val amqpInput = DeserializationInput(serializerFactory)
// Use our helper functions to peek the size of the serialized object out of the AMQP byte stream.
val peekedBytes = input.readBytes(DeserializationInput.BYTES_NEEDED_TO_PEEK)
val size = DeserializationInput.peekSize(peekedBytes)
val allBytes = peekedBytes.copyOf(size)
input.readBytes(allBytes, peekedBytes.size, size - peekedBytes.size)
return amqpInput.deserialize(SerializedBytes<Any>(allBytes), type)
}
}

View File

@ -1,14 +1,16 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import com.google.common.primitives.Primitives import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type import java.lang.reflect.Type
/** /**
* Serializer / deserializer for native AMQP types (Int, Float, String etc). * Serializer / deserializer for native AMQP types (Int, Float, String etc).
*
* [ByteArray] is automatically marshalled to/from the Proton-J wrapper, [Binary].
*/ */
class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> { class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
override val typeDescriptor: String = SerializerFactory.primitiveTypeName(Primitives.wrap(clazz))!! override val typeDescriptor: String = SerializerFactory.primitiveTypeName(clazz)!!
override val type: Type = clazz override val type: Type = clazz
// NOOP since this is a primitive type. // NOOP since this is a primitive type.
@ -16,8 +18,12 @@ class AMQPPrimitiveSerializer(clazz: Class<*>) : AMQPSerializer<Any> {
} }
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) { override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
data.putObject(obj) if (obj is ByteArray) {
data.putObject(Binary(obj))
} else {
data.putObject(obj)
}
} }
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = obj override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any = (obj as? Binary)?.array ?: obj
} }

View File

@ -2,8 +2,6 @@ package net.corda.core.serialization.amqp
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
/** /**
@ -12,14 +10,10 @@ import java.lang.reflect.Type
class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> { class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
internal val elementType: Type = makeElementType() internal val elementType: Type = type.componentType()
private val typeNotation: TypeNotation = RestrictedType(type.typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) private val typeNotation: TypeNotation = RestrictedType(type.typeName, null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
private fun makeElementType(): Type {
return (type as? Class<*>)?.componentType ?: (type as GenericArrayType).genericComponentType
}
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) { if (output.writeTypeNotations(typeNotation)) {
output.requireSerializer(elementType) output.requireSerializer(elementType)
@ -44,13 +38,7 @@ class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQ
} }
private fun <T> List<T>.toArrayOfType(type: Type): Any { private fun <T> List<T>.toArrayOfType(type: Type): Any {
val elementType: Class<*> = if (type is Class<*>) { val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
type
} else if (type is ParameterizedType) {
type.rawType as Class<*>
} else {
throw NotSerializableException("Unexpected array element type $type")
}
val list = this val list = this
return java.lang.reflect.Array.newInstance(elementType, this.size).apply { return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
val array = this val array = this

View File

@ -32,7 +32,7 @@ class CollectionSerializer(val declaredType: ParameterizedType, factory: Seriali
private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>) private val concreteBuilder: (List<*>) -> Collection<*> = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList()) private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "list", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) { if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,5 +1,6 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import net.corda.core.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type import java.lang.reflect.Type
@ -10,11 +11,16 @@ import java.lang.reflect.Type
abstract class CustomSerializer<T> : AMQPSerializer<T> { abstract class CustomSerializer<T> : AMQPSerializer<T> {
/** /**
* This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects * This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects
* that refer to arrays of types etc. * that refer to other custom types etc.
*/ */
abstract val additionalSerializers: Iterable<CustomSerializer<out Any>> abstract val additionalSerializers: Iterable<CustomSerializer<out Any>>
/**
* This method should return true if the custom serializer can serialize an instance of the class passed as the
* parameter.
*/
abstract fun isSerializerFor(clazz: Class<*>): Boolean abstract fun isSerializerFor(clazz: Class<*>): Boolean
protected abstract val descriptor: Descriptor protected abstract val descriptor: Descriptor
/** /**
* This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization * This exists purely for documentation and cross-platform purposes. It is not used by our serialization / deserialization
@ -32,12 +38,42 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) abstract fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput)
/** /**
* Additional base features for a custom serializer that is a particular class. * This custom serializer represents a sort of symbolic link from a subclass to a super class, where the super
* class custom serializer is responsible for the "on the wire" format but we want to create a reference to the
* subclass in the schema, so that we can distinguish between subclasses.
*/
// TODO: should this be a custom serializer at all, or should it just be a plain AMQPSerializer?
class SubClass<T>(protected val clazz: Class<*>, protected val superClassSerializer: CustomSerializer<T>) : CustomSerializer<T>() {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
// TODO: should this be empty or contain the schema of the super?
override val schemaForDocumentation = Schema(emptyList())
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${fingerprintForDescriptors(superClassSerializer.typeDescriptor, nameForType(clazz))}"
private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(clazz), null, emptyList(), SerializerFactory.nameForType(superClassSerializer.type), Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)
}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
superClassSerializer.writeDescribedObject(obj, data, type, output)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
return superClassSerializer.readObject(obj, schema, input)
}
}
/**
* Additional base features for a custom serializer for a particular class, that excludes subclasses.
*/ */
abstract class Is<T>(protected val clazz: Class<T>) : CustomSerializer<T>() { abstract class Is<T>(protected val clazz: Class<T>) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.clazz
override val type: Type get() = clazz override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override fun writeClassInfo(output: SerializationOutput) {} override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor) override val descriptor: Descriptor = Descriptor(typeDescriptor)
} }
@ -48,13 +84,13 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
abstract class Implements<T>(protected val clazz: Class<T>) : CustomSerializer<T>() { abstract class Implements<T>(protected val clazz: Class<T>) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz) override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz)
override val type: Type get() = clazz override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override fun writeClassInfo(output: SerializationOutput) {} override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor) override val descriptor: Descriptor = Descriptor(typeDescriptor)
} }
/** /**
* Addition base features over and above [Implements] or [Is] custom serializer for when the serialize form should be * Additional base features over and above [Implements] or [Is] custom serializer for when the serialized form should be
* the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization. * the serialized form of a proxy class, and the object can be re-created from that proxy on deserialization.
* *
* The proxy class must use only types which are either native AMQP or other types for which there are pre-registered * The proxy class must use only types which are either native AMQP or other types for which there are pre-registered
@ -66,14 +102,14 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
val withInheritance: Boolean = true) : CustomSerializer<T>() { val withInheritance: Boolean = true) : CustomSerializer<T>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
override val type: Type get() = clazz override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}" override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override fun writeClassInfo(output: SerializationOutput) {} override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor) override val descriptor: Descriptor = Descriptor(typeDescriptor)
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) } private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
override val schemaForDocumentation: Schema by lazy { override val schemaForDocumentation: Schema by lazy {
val typeNotations = mutableSetOf<TypeNotation>(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields)) val typeNotations = mutableSetOf<TypeNotation>(CompositeType(nameForType(type), null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields))
for (additional in additionalSerializers) { for (additional in additionalSerializers) {
typeNotations.addAll(additional.schemaForDocumentation.types) typeNotations.addAll(additional.schemaForDocumentation.types)
} }
@ -102,4 +138,38 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
return fromProxy(proxy) return fromProxy(proxy)
} }
} }
/**
* A custom serializer where the on-wire representation is a string. For example, a [Currency] might be represented
* as a 3 character currency code, and converted to and from that string. By default, it is assumed that the
* [toString] method will generate the string representation and that there is a constructor that takes such a
* string as an argument to reconstruct.
*
* @param clazz The type to be marshalled
* @param withInheritance Whether subclasses of the class can also be marshalled.
* @param make A lambda for constructing an instance, that defaults to calling a constructor that expects a string.
* @param unmake A lambda that extracts the string value for an instance, that defaults to the [toString] method.
*/
abstract class ToString<T>(clazz: Class<T>, withInheritance: Boolean = false,
private val maker: (String) -> T = clazz.getConstructor(String::class.java).let { `constructor` -> { string -> `constructor`.newInstance(string) } },
private val unmaker: (T) -> String = { obj -> obj.toString() }) : Proxy<T, String>(clazz, String::class.java, /* Unused */ SerializerFactory(), withInheritance) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
override val schemaForDocumentation = Schema(listOf(RestrictedType(nameForType(type), "", listOf(nameForType(type)), SerializerFactory.primitiveTypeName(String::class.java)!!, descriptor, emptyList())))
override fun toProxy(obj: T): String = unmaker(obj)
override fun fromProxy(proxy: String): T = maker(proxy)
override fun writeDescribedObject(obj: T, data: Data, type: Type, output: SerializationOutput) {
val proxy = toProxy(obj)
data.putObject(proxy)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): T {
val proxy = input.readObject(obj, schema, String::class.java) as String
return fromProxy(proxy)
}
}
} }

View File

@ -2,13 +2,17 @@ package net.corda.core.serialization.amqp
import com.google.common.base.Throwables import com.google.common.base.Throwables
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.UnsignedByte
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.reflect.Type import java.lang.reflect.Type
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.*
data class objectAndEnvelope<T>(val obj: T, val envelope: Envelope)
/** /**
* Main entry point for deserializing an AMQP encoded object. * Main entry point for deserializing an AMQP encoded object.
* *
@ -19,8 +23,80 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
// TODO: we're not supporting object refs yet // TODO: we're not supporting object refs yet
private val objectHistory: MutableList<Any> = ArrayList() private val objectHistory: MutableList<Any> = ArrayList()
internal companion object {
val BYTES_NEEDED_TO_PEEK: Int = 23
private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean {
if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException()
var bytesRemaining = length
var aPos = aOffset
var bPos = bOffset
while (bytesRemaining-- > 0) {
if (a[aPos++] != b[bPos++]) return false
}
return true
}
fun peekSize(bytes: ByteArray): Int {
// There's an 8 byte header, and then a 0 byte plus descriptor followed by constructor
val eighth = bytes[8].toInt()
check(eighth == 0x0) { "Expected to find a descriptor in the AMQP stream" }
// We should always have an Envelope, so the descriptor should be a 64-bit long (0x80)
val ninth = UnsignedByte.valueOf(bytes[9]).toInt()
check(ninth == 0x80) { "Expected to find a ulong in the AMQP stream" }
// Skip 8 bytes
val eighteenth = UnsignedByte.valueOf(bytes[18]).toInt()
check(eighteenth == 0xd0 || eighteenth == 0xc0) { "Expected to find a list8 or list32 in the AMQP stream" }
val size = if (eighteenth == 0xc0) {
// Next byte is size
UnsignedByte.valueOf(bytes[19]).toInt() - 3 // Minus three as PEEK_SIZE assumes 4 byte unsigned integer.
} else {
// Next 4 bytes is size
UnsignedByte.valueOf(bytes[19]).toInt().shl(24) + UnsignedByte.valueOf(bytes[20]).toInt().shl(16) + UnsignedByte.valueOf(bytes[21]).toInt().shl(8) + UnsignedByte.valueOf(bytes[22]).toInt()
}
return size + BYTES_NEEDED_TO_PEEK
}
}
@Throws(NotSerializableException::class) @Throws(NotSerializableException::class)
inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T = deserialize(bytes, T::class.java) inline fun <reified T : Any> deserialize(bytes: SerializedBytes<T>): T =
deserialize(bytes, T::class.java)
@Throws(NotSerializableException::class)
inline internal fun <reified T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>): objectAndEnvelope<T> =
deserializeAndReturnEnvelope(bytes, T::class.java)
@Throws(NotSerializableException::class)
private fun <T : Any> getEnvelope(bytes: SerializedBytes<T>): Envelope {
// Check that the lead bytes match expected header
if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) {
throw NotSerializableException("Serialization header does not match.")
}
val data = Data.Factory.create()
val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8))
if (size.toInt() != bytes.size - 8) {
throw NotSerializableException("Unexpected size of data")
}
return Envelope.get(data)
}
@Throws(NotSerializableException::class)
private fun <T : Any, R> des(bytes: SerializedBytes<T>, clazz: Class<T>, generator: (SerializedBytes<T>, Class<T>) -> R): R {
try {
return generator(bytes, clazz)
} catch(nse: NotSerializableException) {
throw nse
} catch(t: Throwable) {
throw NotSerializableException("Unexpected throwable: ${t.message} ${Throwables.getStackTraceAsString(t)}")
} finally {
objectHistory.clear()
}
}
/** /**
* This is the main entry point for deserialization of AMQP payloads, and expects a byte sequence involving a header * This is the main entry point for deserialization of AMQP payloads, and expects a byte sequence involving a header
@ -29,25 +105,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
*/ */
@Throws(NotSerializableException::class) @Throws(NotSerializableException::class)
fun <T : Any> deserialize(bytes: SerializedBytes<T>, clazz: Class<T>): T { fun <T : Any> deserialize(bytes: SerializedBytes<T>, clazz: Class<T>): T {
try { return des<T, T>(bytes, clazz) { bytes, clazz ->
// Check that the lead bytes match expected header var envelope = getEnvelope(bytes)
if (!subArraysEqual(bytes.bytes, 0, 8, AmqpHeaderV1_0.bytes, 0)) { clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
throw NotSerializableException("Serialization header does not match.") }
} }
val data = Data.Factory.create()
val size = data.decode(ByteBuffer.wrap(bytes.bytes, 8, bytes.size - 8)) @Throws(NotSerializableException::class)
if (size.toInt() != bytes.size - 8) { internal fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): objectAndEnvelope<T> {
throw NotSerializableException("Unexpected size of data") return des<T, objectAndEnvelope<T>>(bytes, clazz) { bytes, clazz ->
} val envelope = getEnvelope(bytes)
val envelope = Envelope.get(data)
// Now pick out the obj and schema from the envelope. // Now pick out the obj and schema from the envelope.
return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)) objectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
} catch(nse: NotSerializableException) {
throw nse
} catch(t: Throwable) {
throw NotSerializableException("Unexpected throwable: ${t.message} ${Throwables.getStackTraceAsString(t)}")
} finally {
objectHistory.clear()
} }
} }
@ -66,25 +135,10 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
if (serializer.type != type && !serializer.type.isSubClassOf(type)) if (serializer.type != type && !serializer.type.isSubClassOf(type))
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type") throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
return serializer.readObject(obj.described, schema, this) return serializer.readObject(obj.described, schema, this)
} else if (obj is Binary) {
return obj.array
} else { } else {
return obj return obj
} }
} }
}
private fun Type.isSubClassOf(type: Type): Boolean {
return type == Object::class.java ||
(this is Class<*> && type is Class<*> && type.isAssignableFrom(this)) ||
(this is DeserializedParameterizedType && type is Class<*> && this.rawType == type && this.isFullyWildcarded)
}
private fun subArraysEqual(a: ByteArray, aOffset: Int, length: Int, b: ByteArray, bOffset: Int): Boolean {
if (aOffset + length > a.size || bOffset + length > b.size) throw IndexOutOfBoundsException()
var bytesRemaining = length
var aPos = aOffset
var bPos = bOffset
while (bytesRemaining-- > 0) {
if (a[aPos++] != b[bPos++]) return false
}
return true
}
}

View File

@ -1,5 +1,6 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import com.google.common.primitives.Primitives
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
@ -119,7 +120,9 @@ class DeserializedParameterizedType(private val rawType: Class<*>, private val p
private fun makeType(typeName: String, cl: ClassLoader): Type { private fun makeType(typeName: String, cl: ClassLoader): Type {
// Not generic // Not generic
return if (typeName == "?") SerializerFactory.AnyType else Class.forName(typeName, false, cl) return if (typeName == "?") SerializerFactory.AnyType else {
Primitives.wrap(SerializerFactory.primitiveType(typeName) ?: Class.forName(typeName, false, cl))
}
} }
private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type { private fun makeParameterizedType(rawTypeName: String, args: MutableList<Type>, cl: ClassLoader): Type {

View File

@ -31,7 +31,7 @@ class MapSerializer(val declaredType: ParameterizedType, factory: SerializerFact
private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>) private val concreteBuilder: (Map<*, *>) -> Map<*, *> = findConcreteType(declaredType.rawType as Class<*>)
private val typeNotation: TypeNotation = RestrictedType(declaredType.toString(), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList()) private val typeNotation: TypeNotation = RestrictedType(SerializerFactory.nameForType(declaredType), null, emptyList(), "map", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) { if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,5 +1,6 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import net.corda.core.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.amqp.UnsignedInteger import org.apache.qpid.proton.amqp.UnsignedInteger
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException import java.io.NotSerializableException
@ -10,7 +11,7 @@ import kotlin.reflect.jvm.javaConstructor
/** /**
* Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor). * Responsible for serializing and deserializing a regular object instance via a series of properties (matched with a constructor).
*/ */
class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> { class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz override val type: Type get() = clazz
private val javaConstructor: Constructor<Any>? private val javaConstructor: Constructor<Any>?
internal val propertySerializers: Collection<PropertySerializer> internal val propertySerializers: Collection<PropertySerializer>
@ -20,7 +21,9 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe
javaConstructor = kotlinConstructor?.javaConstructor javaConstructor = kotlinConstructor?.javaConstructor
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory) propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
} }
private val typeName = clazz.name
private val typeName = nameForType(clazz)
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}" override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict. private val interfaces = interfacesForSerialization(clazz) // TODO maybe this proves too much and we need annotations to restrict.
@ -65,7 +68,7 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe
} }
private fun generateProvides(): List<String> { private fun generateProvides(): List<String> {
return interfaces.map { it.typeName } return interfaces.map { nameForType(it) }
} }

View File

@ -1,14 +1,16 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Method import java.lang.reflect.Method
import java.lang.reflect.Type
import kotlin.reflect.full.memberProperties import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter import kotlin.reflect.jvm.javaGetter
/** /**
* Base class for serialization of a property of an object. * Base class for serialization of a property of an object.
*/ */
sealed class PropertySerializer(val name: String, val readMethod: Method) { sealed class PropertySerializer(val name: String, val readMethod: Method, val resolvedType: Type) {
abstract fun writeClassInfo(output: SerializationOutput) abstract fun writeClassInfo(output: SerializationOutput)
abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) abstract fun writeProperty(obj: Any?, data: Data, output: SerializationOutput)
abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? abstract fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any?
@ -18,23 +20,20 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
val default: String? = generateDefault() val default: String? = generateDefault()
val mandatory: Boolean = generateMandatory() val mandatory: Boolean = generateMandatory()
private val isInterface: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isInterface ?: false private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false
private val isJVMPrimitive: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isPrimitive ?: false private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false
private fun generateType(): String { private fun generateType(): String {
return if (isInterface) "*" else { return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType)
val primitiveName = SerializerFactory.primitiveTypeName(readMethod.genericReturnType)
return primitiveName ?: readMethod.genericReturnType.typeName
}
} }
private fun generateRequires(): List<String> { private fun generateRequires(): List<String> {
return if (isInterface) listOf(readMethod.genericReturnType.typeName) else emptyList() return if (isInterface) listOf(SerializerFactory.nameForType(resolvedType)) else emptyList()
} }
private fun generateDefault(): String? { private fun generateDefault(): String? {
if (isJVMPrimitive) { if (isJVMPrimitive) {
return when (readMethod.genericReturnType) { return when (resolvedType) {
java.lang.Boolean.TYPE -> "false" java.lang.Boolean.TYPE -> "false"
java.lang.Character.TYPE -> "&#0" java.lang.Character.TYPE -> "&#0"
else -> "0" else -> "0"
@ -54,13 +53,12 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
} }
companion object { companion object {
fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer { fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
val type = readMethod.genericReturnType if (SerializerFactory.isPrimitive(resolvedType)) {
if (SerializerFactory.isPrimitive(type)) {
// This is a little inefficient for performance since it does a runtime check of type. We could do build time check with lots of subclasses here. // This is a little inefficient for performance since it does a runtime check of type. We could do build time check with lots of subclasses here.
return AMQPPrimitivePropertySerializer(name, readMethod) return AMQPPrimitivePropertySerializer(name, readMethod, resolvedType)
} else { } else {
return DescribedTypePropertySerializer(name, readMethod) { factory.get(null, type) } return DescribedTypePropertySerializer(name, readMethod, resolvedType) { factory.get(null, resolvedType) }
} }
} }
} }
@ -68,35 +66,43 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
/** /**
* A property serializer for a complex type (another object). * A property serializer for a complex type (another object).
*/ */
class DescribedTypePropertySerializer(name: String, readMethod: Method, private val lazyTypeSerializer: () -> AMQPSerializer<Any>) : PropertySerializer(name, readMethod) { class DescribedTypePropertySerializer(name: String, readMethod: Method, resolvedType: Type, private val lazyTypeSerializer: () -> AMQPSerializer<*>) : PropertySerializer(name, readMethod, resolvedType) {
// This is lazy so we don't get an infinite loop when a method returns an instance of the class. // This is lazy so we don't get an infinite loop when a method returns an instance of the class.
private val typeSerializer: AMQPSerializer<Any> by lazy { lazyTypeSerializer() } private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
override fun writeClassInfo(output: SerializationOutput) { override fun writeClassInfo(output: SerializationOutput) {
typeSerializer.writeClassInfo(output) if (resolvedType != Any::class.java) {
typeSerializer.writeClassInfo(output)
}
} }
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
return input.readObjectOrNull(obj, schema, readMethod.genericReturnType) return input.readObjectOrNull(obj, schema, resolvedType)
} }
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
output.writeObjectOrNull(readMethod.invoke(obj), data, readMethod.genericReturnType) output.writeObjectOrNull(readMethod.invoke(obj), data, resolvedType)
} }
} }
/** /**
* A property serializer for an AMQP primitive type (Int, String, etc). * A property serializer for an AMQP primitive type (Int, String, etc).
*/ */
class AMQPPrimitivePropertySerializer(name: String, readMethod: Method) : PropertySerializer(name, readMethod) { class AMQPPrimitivePropertySerializer(name: String, readMethod: Method, resolvedType: Type) : PropertySerializer(name, readMethod, resolvedType) {
override fun writeClassInfo(output: SerializationOutput) {} override fun writeClassInfo(output: SerializationOutput) {}
override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? { override fun readProperty(obj: Any?, schema: Schema, input: DeserializationInput): Any? {
return obj return if (obj is Binary) obj.array else obj
} }
override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) { override fun writeProperty(obj: Any?, data: Data, output: SerializationOutput) {
data.putObject(readMethod.invoke(obj)) val value = readMethod.invoke(obj)
if (value is ByteArray) {
data.putObject(Binary(value))
} else {
data.putObject(value)
}
} }
} }
} }

View File

@ -2,8 +2,8 @@ package net.corda.core.serialization.amqp
import com.google.common.hash.Hasher import com.google.common.hash.Hasher
import com.google.common.hash.Hashing import com.google.common.hash.Hashing
import net.corda.core.crypto.Base58 import net.corda.core.crypto.toBase64
import net.corda.core.serialization.OpaqueBytes import net.corda.core.utilities.OpaqueBytes
import org.apache.qpid.proton.amqp.DescribedType import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.UnsignedLong import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
@ -12,6 +12,8 @@ import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
import java.lang.reflect.TypeVariable
import java.util.*
// TODO: get an assigned number as per AMQP spec // TODO: get an assigned number as per AMQP spec
val DESCRIPTOR_TOP_32BITS: Long = 0xc0da0000 val DESCRIPTOR_TOP_32BITS: Long = 0xc0da0000
@ -310,6 +312,7 @@ private val ALREADY_SEEN_HASH: String = "Already seen = true"
private val NULLABLE_HASH: String = "Nullable = true" private val NULLABLE_HASH: String = "Nullable = true"
private val NOT_NULLABLE_HASH: String = "Nullable = false" private val NOT_NULLABLE_HASH: String = "Nullable = false"
private val ANY_TYPE_HASH: String = "Any type = true" private val ANY_TYPE_HASH: String = "Any type = true"
private val TYPE_VARIABLE_HASH: String = "Type variable = true"
/** /**
* The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation. * The method generates a fingerprint for a given JVM [Type] that should be unique to the schema representation.
@ -320,44 +323,83 @@ private val ANY_TYPE_HASH: String = "Any type = true"
* different. * different.
*/ */
// TODO: write tests // TODO: write tests
internal fun fingerprintForType(type: Type, factory: SerializerFactory): String = Base58.encode(fingerprintForType(type, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes()) internal fun fingerprintForType(type: Type, factory: SerializerFactory): String {
return fingerprintForType(type, null, HashSet(), Hashing.murmur3_128().newHasher(), factory).hash().asBytes().toBase64()
}
private fun fingerprintForType(type: Type, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher { internal fun fingerprintForDescriptors(vararg typeDescriptors: String): String {
val hasher = Hashing.murmur3_128().newHasher()
for (typeDescriptor in typeDescriptors) {
hasher.putUnencodedChars(typeDescriptor)
}
return hasher.hash().asBytes().toBase64()
}
// This method concatentates various elements of the types recursively as unencoded strings into the hasher, effectively
// creating a unique string for a type which we then hash in the calling function above.
private fun fingerprintForType(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
return if (type in alreadySeen) { return if (type in alreadySeen) {
hasher.putUnencodedChars(ALREADY_SEEN_HASH) hasher.putUnencodedChars(ALREADY_SEEN_HASH)
} else { } else {
alreadySeen += type alreadySeen += type
if (type is SerializerFactory.AnyType) { try {
hasher.putUnencodedChars(ANY_TYPE_HASH) if (type is SerializerFactory.AnyType) {
} else if (type is Class<*>) { hasher.putUnencodedChars(ANY_TYPE_HASH)
if (type.isArray) { } else if (type is Class<*>) {
fingerprintForType(type.componentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH) if (type.isArray) {
} else if (SerializerFactory.isPrimitive(type)) { fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
hasher.putUnencodedChars(type.name) } else if (SerializerFactory.isPrimitive(type)) {
} else if (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) { hasher.putUnencodedChars(type.name)
hasher.putUnencodedChars(type.name) } else if (isCollectionOrMap(type)) {
} else { hasher.putUnencodedChars(type.name)
// Need to check if a custom serializer is applicable
val customSerializer = factory.findCustomSerializer(type)
if (customSerializer == null) {
// Hash the class + properties + interfaces
propertiesForSerialization(constructorForDeserialization(type), type, factory).fold(hasher.putUnencodedChars(type.name)) { orig, param ->
fingerprintForType(param.readMethod.genericReturnType, alreadySeen, orig, factory).putUnencodedChars(param.name).putUnencodedChars(if (param.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
}
interfacesForSerialization(type).map { fingerprintForType(it, alreadySeen, hasher, factory) }
hasher
} else { } else {
hasher.putUnencodedChars(customSerializer.typeDescriptor) // Need to check if a custom serializer is applicable
val customSerializer = factory.findCustomSerializer(type, type)
if (customSerializer == null) {
if (type.kotlin.objectInstance != null) {
// TODO: name collision is too likely for kotlin objects, we need to introduce some reference
// to the CorDapp but maybe reference to the JAR in the short term.
hasher.putUnencodedChars(type.name)
} else {
fingerprintForObject(type, contextType, alreadySeen, hasher, factory)
}
} else {
hasher.putUnencodedChars(customSerializer.typeDescriptor)
}
} }
} else if (type is ParameterizedType) {
// Hash the rawType + params
val clazz = type.rawType as Class<*>
val startingHash = if (isCollectionOrMap(clazz)) {
hasher.putUnencodedChars(clazz.name)
} else {
fingerprintForObject(type, type, alreadySeen, hasher, factory)
}
// ... and concatentate the type data for each parameter type.
type.actualTypeArguments.fold(startingHash) { orig, paramType -> fingerprintForType(paramType, type, alreadySeen, orig, factory) }
} else if (type is GenericArrayType) {
// Hash the element type + some array hash
fingerprintForType(type.genericComponentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (type is TypeVariable<*>) {
// TODO: include bounds
hasher.putUnencodedChars(type.name).putUnencodedChars(TYPE_VARIABLE_HASH)
} else {
throw NotSerializableException("Don't know how to hash")
} }
} else if (type is ParameterizedType) { } catch(e: NotSerializableException) {
// Hash the rawType + params throw NotSerializableException("${e.message} -> $type")
type.actualTypeArguments.fold(fingerprintForType(type.rawType, alreadySeen, hasher, factory)) { orig, paramType -> fingerprintForType(paramType, alreadySeen, orig, factory) }
} else if (type is GenericArrayType) {
// Hash the element type + some array hash
fingerprintForType(type.genericComponentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else {
throw NotSerializableException("Don't know how to hash $type")
} }
} }
} }
private fun isCollectionOrMap(type: Class<*>) = Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)
private fun fingerprintForObject(type: Type, contextType: Type?, alreadySeen: MutableSet<Type>, hasher: Hasher, factory: SerializerFactory): Hasher {
// Hash the class + properties + interfaces
val name = type.asClass()?.name ?: throw NotSerializableException("Expected only Class or ParameterizedType but found $type")
propertiesForSerialization(constructorForDeserialization(type), contextType ?: type, factory).fold(hasher.putUnencodedChars(name)) { orig, prop ->
fingerprintForType(prop.resolvedType, type, alreadySeen, orig, factory).putUnencodedChars(prop.name).putUnencodedChars(if (prop.mandatory) NOT_NULLABLE_HASH else NULLABLE_HASH)
}
interfacesForSerialization(type).map { fingerprintForType(it, type, alreadySeen, hasher, factory) }
return hasher
}

View File

@ -4,10 +4,8 @@ import com.google.common.reflect.TypeToken
import org.apache.qpid.proton.codec.Data import org.apache.qpid.proton.codec.Data
import java.beans.Introspector import java.beans.Introspector
import java.io.NotSerializableException import java.io.NotSerializableException
import java.lang.reflect.Method import java.lang.reflect.*
import java.lang.reflect.Modifier import java.util.*
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KFunction import kotlin.reflect.KFunction
import kotlin.reflect.KParameter import kotlin.reflect.KParameter
@ -29,9 +27,10 @@ annotation class ConstructorForDeserialization
* Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is * Otherwise it starts with the primary constructor in kotlin, if there is one, and then will override this with any that is
* annotated with [@CordaConstructor]. It will report an error if more than one constructor is annotated. * annotated with [@CordaConstructor]. It will report an error if more than one constructor is annotated.
*/ */
internal fun <T : Any> constructorForDeserialization(clazz: Class<T>): KFunction<T>? { internal fun constructorForDeserialization(type: Type): KFunction<Any>? {
val clazz: Class<*> = type.asClass()!!
if (isConcrete(clazz)) { if (isConcrete(clazz)) {
var preferredCandidate: KFunction<T>? = clazz.kotlin.primaryConstructor var preferredCandidate: KFunction<Any>? = clazz.kotlin.primaryConstructor
var annotatedCount = 0 var annotatedCount = 0
val kotlinConstructors = clazz.kotlin.constructors val kotlinConstructors = clazz.kotlin.constructors
val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() } val hasDefault = kotlinConstructors.any { it.parameters.isEmpty() }
@ -60,13 +59,14 @@ internal fun <T : Any> constructorForDeserialization(clazz: Class<T>): KFunction
* Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have * Note, you will need any Java classes to be compiled with the `-parameters` option to ensure constructor parameters have
* names accessible via reflection. * names accessible via reflection.
*/ */
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, clazz: Class<*>, factory: SerializerFactory): Collection<PropertySerializer> { internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory) val clazz = type.asClass()!!
return if (kotlinConstructor != null) propertiesForSerializationFromConstructor(kotlinConstructor, type, factory) else propertiesForSerializationFromAbstract(clazz, type, factory)
} }
private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers)) private fun isConcrete(clazz: Class<*>): Boolean = !(clazz.isInterface || Modifier.isAbstract(clazz.modifiers))
private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>, factory: SerializerFactory): Collection<PropertySerializer> { private fun <T : Any> propertiesForSerializationFromConstructor(kotlinConstructor: KFunction<T>, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType val clazz = (kotlinConstructor.returnType.classifier as KClass<*>).javaObjectType
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] } val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.groupBy { it.name }.mapValues { it.value[0] }
@ -78,10 +78,11 @@ private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>
// Check that the method has a getter in java. // Check that the method has a getter in java.
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." + val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." +
" If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.") " If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
val returnType = resolveTypeVariables(getter.genericReturnType, type)
if (constructorParamTakesReturnTypeOfGetter(getter, param)) { if (constructorParamTakesReturnTypeOfGetter(getter, param)) {
rc += PropertySerializer.make(name, getter, factory) rc += PropertySerializer.make(name, getter, returnType, factory)
} else { } else {
throw NotSerializableException("Property type ${getter.genericReturnType} for $name of $clazz differs from constructor parameter type ${param.type.javaType}") throw NotSerializableException("Property type $returnType for $name of $clazz differs from constructor parameter type ${param.type.javaType}")
} }
} }
return rc return rc
@ -89,35 +90,36 @@ private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>
private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType) private fun constructorParamTakesReturnTypeOfGetter(getter: Method, param: KParameter): Boolean = TypeToken.of(param.type.javaType).isSupertypeOf(getter.genericReturnType)
private fun propertiesForSerialization(clazz: Class<*>, factory: SerializerFactory): Collection<PropertySerializer> { private fun propertiesForSerializationFromAbstract(clazz: Class<*>, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
// Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans. // Kotlin reflection doesn't work with Java getters the way you might expect, so we drop back to good ol' beans.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name } val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }
val rc: MutableList<PropertySerializer> = ArrayList(properties.size) val rc: MutableList<PropertySerializer> = ArrayList(properties.size)
for (property in properties) { for (property in properties) {
// Check that the method has a getter in java. // Check that the method has a getter in java.
val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.") val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.")
rc += PropertySerializer.make(property.name, getter, factory) val returnType = resolveTypeVariables(getter.genericReturnType, type)
rc += PropertySerializer.make(property.name, getter, returnType, factory)
} }
return rc return rc
} }
internal fun interfacesForSerialization(clazz: Class<*>): List<Type> { internal fun interfacesForSerialization(type: Type): List<Type> {
val interfaces = LinkedHashSet<Type>() val interfaces = LinkedHashSet<Type>()
exploreType(clazz, interfaces) exploreType(type, interfaces)
return interfaces.toList() return interfaces.toList()
} }
private fun exploreType(type: Type?, interfaces: MutableSet<Type>) { private fun exploreType(type: Type?, interfaces: MutableSet<Type>) {
val clazz = (type as? Class<*>) ?: (type as? ParameterizedType)?.rawType as? Class<*> val clazz = type?.asClass()
if (clazz != null) { if (clazz != null) {
if (clazz.isInterface) interfaces += clazz if (clazz.isInterface) interfaces += type!!
for (newInterface in clazz.genericInterfaces) { for (newInterface in clazz.genericInterfaces) {
if (newInterface !in interfaces) { if (newInterface !in interfaces) {
interfaces += newInterface exploreType(resolveTypeVariables(newInterface, type), interfaces)
exploreType(newInterface, interfaces)
} }
} }
exploreType(clazz.genericSuperclass, interfaces) val superClass = clazz.genericSuperclass ?: return
exploreType(resolveTypeVariables(superClass, type), interfaces)
} }
} }
@ -143,4 +145,58 @@ fun Data.withList(block: Data.() -> Unit) {
enter() enter()
block() block()
exit() // exit list exit() // exit list
}
private fun resolveTypeVariables(actualType: Type, contextType: Type?): Type {
val resolvedType = if (contextType != null) TypeToken.of(contextType).resolveType(actualType).type else actualType
// TODO: surely we check it is concrete at this point with no TypeVariables
return if (resolvedType is TypeVariable<*>) {
val bounds = resolvedType.bounds
return if (bounds.isEmpty()) SerializerFactory.AnyType else if (bounds.size == 1) resolveTypeVariables(bounds[0], contextType) else throw NotSerializableException("Got bounded type $actualType but only support single bound.")
} else {
resolvedType
}
}
internal fun Type.asClass(): Class<*>? {
return if (this is Class<*>) {
this
} else if (this is ParameterizedType) {
this.rawType.asClass()
} else if (this is GenericArrayType) {
this.genericComponentType.asClass()?.arrayClass()
} else null
}
internal fun Type.asArray(): Type? {
return if (this is Class<*>) {
this.arrayClass()
} else if (this is ParameterizedType) {
DeserializedGenericArrayType(this)
} else null
}
internal fun Class<*>.arrayClass(): Class<*> = java.lang.reflect.Array.newInstance(this, 0).javaClass
internal fun Type.isArray(): Boolean = (this is Class<*> && this.isArray) || (this is GenericArrayType)
internal fun Type.componentType(): Type {
check(this.isArray()) { "$this is not an array type." }
return (this as? Class<*>)?.componentType ?: (this as GenericArrayType).genericComponentType
}
internal fun Class<*>.asParameterizedType(): ParameterizedType {
return DeserializedParameterizedType(this, this.typeParameters)
}
internal fun Type.asParameterizedType(): ParameterizedType {
return when (this) {
is Class<*> -> this.asParameterizedType()
is ParameterizedType -> this
else -> throw NotSerializableException("Don't know how to convert to ParameterizedType")
}
}
internal fun Type.isSubClassOf(type: Type): Boolean {
return TypeToken.of(this).isSubtypeOf(type)
} }

View File

@ -1,6 +1,7 @@
package net.corda.core.serialization.amqp package net.corda.core.serialization.amqp
import com.google.common.primitives.Primitives import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeResolver
import net.corda.core.checkNotUnorderedHashMap import net.corda.core.checkNotUnorderedHashMap
import net.corda.core.serialization.AllWhitelist import net.corda.core.serialization.AllWhitelist
import net.corda.core.serialization.ClassWhitelist import net.corda.core.serialization.ClassWhitelist
@ -20,9 +21,9 @@ import javax.annotation.concurrent.ThreadSafe
* Factory of serializers designed to be shared across threads and invocations. * Factory of serializers designed to be shared across threads and invocations.
*/ */
// TODO: enums // TODO: enums
// TODO: object references // TODO: object references - need better fingerprinting?
// TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal) // TODO: class references? (e.g. cheat with repeated descriptors using a long encoding, like object ref proposal)
// TODO: Inner classes etc // TODO: Inner classes etc. Should we allow? Currently not considered.
// TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency // TODO: support for intern-ing of deserialized objects for some core types (e.g. PublicKey) for memory efficiency
// TODO: maybe support for caching of serialized form of some core types for performance // TODO: maybe support for caching of serialized form of some core types for performance
// TODO: profile for performance in general // TODO: profile for performance in general
@ -32,7 +33,13 @@ import javax.annotation.concurrent.ThreadSafe
// TODO: apply class loader logic and an "app context" throughout this code. // TODO: apply class loader logic and an "app context" throughout this code.
// TODO: schema evolution solution when the fingerprints do not line up. // TODO: schema evolution solution when the fingerprints do not line up.
// TODO: allow definition of well known types that are left out of the schema. // TODO: allow definition of well known types that are left out of the schema.
// TODO: automatically support byte[] without having to wrap in [Binary]. // TODO: generally map Object to '*' all over the place in the schema and make sure use of '*' amd '?' is consistent and documented in generics.
// TODO: found a document that states textual descriptors are Symbols. Adjust schema class appropriately.
// TODO: document and alert to the fact that classes cannot default superclass/interface properties otherwise they are "erased" due to matching with constructor.
// TODO: type name prefixes for interfaces and abstract classes? Or use label?
// TODO: generic types should define restricted type alias with source of the wildcarded version, I think, if we're to generate classes from schema
// TODO: need to rethink matching of constructor to properties in relation to implementing interfaces and needing those properties etc.
// TODO: need to support super classes as well as interfaces with our current code base... what's involved? If we continue to ban, what is the impact?
@ThreadSafe @ThreadSafe
class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) { class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>() private val serializersByType = ConcurrentHashMap<Type, AMQPSerializer<Any>>()
@ -42,44 +49,99 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
/** /**
* Look up, and manufacture if necessary, a serializer for the given type. * Look up, and manufacture if necessary, a serializer for the given type.
* *
* @param actualType Will be null if there isn't an actual object instance available (e.g. for * @param actualClass Will be null if there isn't an actual object instance available (e.g. for
* restricted type processing). * restricted type processing).
*/ */
@Throws(NotSerializableException::class) @Throws(NotSerializableException::class)
fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer<Any> { fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
if (declaredType is ParameterizedType) { val declaredClass = declaredType.asClass()
return serializersByType.computeIfAbsent(declaredType) { if (declaredClass != null) {
// We allow only Collection and Map. val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
val rawType = declaredType.rawType if (Collection::class.java.isAssignableFrom(declaredClass)) {
if (rawType is Class<*>) { return serializersByType.computeIfAbsent(declaredType) {
checkParameterisedTypesConcrete(declaredType.actualTypeArguments) CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType), null), this)
if (Collection::class.java.isAssignableFrom(rawType)) { }
CollectionSerializer(declaredType, this) } else if (Map::class.java.isAssignableFrom(declaredClass)) {
} else if (Map::class.java.isAssignableFrom(rawType)) { return serializersByType.computeIfAbsent(declaredClass) {
makeMapSerializer(declaredType) makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType, AnyType), null))
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
}
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
} }
}
} else if (declaredType is Class<*>) {
// Simple classes allowed
if (Collection::class.java.isAssignableFrom(declaredType)) {
return serializersByType.computeIfAbsent(declaredType) { CollectionSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType), null), this) }
} else if (Map::class.java.isAssignableFrom(declaredType)) {
return serializersByType.computeIfAbsent(declaredType) { makeMapSerializer(DeserializedParameterizedType(declaredType, arrayOf(AnyType, AnyType), null)) }
} else { } else {
return makeClassSerializer(actualType ?: declaredType) return makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
} }
} else if (declaredType is GenericArrayType) {
return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) }
} else { } else {
throw NotSerializableException("Declared types of $declaredType are not supported.") throw NotSerializableException("Declared types of $declaredType are not supported.")
} }
} }
/**
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
* type.
*/
// TODO: test GenericArrayType
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: Type): Type? {
if (declaredType is ParameterizedType) {
return inferTypeVariables(actualClass, declaredClass, declaredType)
} else if (declaredType is Class<*>) {
// Nothing to infer, otherwise we'd have ParameterizedType
return actualClass
} else if (declaredType is GenericArrayType) {
val declaredComponent = declaredType.genericComponentType
return inferTypeVariables(actualClass?.componentType, declaredComponent.asClass()!!, declaredComponent)?.asArray()
} else return null
}
/**
* Try and infer concrete types for any generics type variables for the actual class encountered, based on the declared
* type, which must be a [ParameterizedType].
*/
private fun inferTypeVariables(actualClass: Class<*>?, declaredClass: Class<*>, declaredType: ParameterizedType): Type? {
if (actualClass == null || declaredClass == actualClass) {
return null
} else if (declaredClass.isAssignableFrom(actualClass)) {
return if (actualClass.typeParameters.isNotEmpty()) {
// The actual class can never have type variables resolved, due to the JVM's use of type erasure, so let's try and resolve them
// Search for declared type in the inheritance hierarchy and then see if that fills in all the variables
val implementationChain: List<Type>? = findPathToDeclared(actualClass, declaredType, mutableListOf<Type>())
if (implementationChain != null) {
val start = implementationChain.last()
val rest = implementationChain.dropLast(1).drop(1)
val resolver = rest.reversed().fold(TypeResolver().where(start, declaredType)) {
resolved, chainEntry ->
val newResolved = resolved.resolveType(chainEntry)
TypeResolver().where(chainEntry, newResolved)
}
// The end type is a special case as it is a Class, so we need to fake up a ParameterizedType for it to get the TypeResolver to do anything.
val endType = DeserializedParameterizedType(actualClass, actualClass.typeParameters)
val resolvedType = resolver.resolveType(endType)
resolvedType
} else throw NotSerializableException("No inheritance path between actual $actualClass and declared $declaredType.")
} else actualClass
} else throw NotSerializableException("Found object of type $actualClass in a property expecting $declaredType")
}
// Stop when reach declared type or return null if we don't find it.
private fun findPathToDeclared(startingType: Type, declaredType: Type, chain: MutableList<Type>): List<Type>? {
chain.add(startingType)
val startingClass = startingType.asClass()
if (startingClass == declaredType.asClass()) {
// We're done...
return chain
}
// Now explore potential options of superclass and all interfaces
val superClass = startingClass?.genericSuperclass
val superClassChain = if (superClass != null) {
val resolved = TypeResolver().where(startingClass.asParameterizedType(), startingType.asParameterizedType()).resolveType(superClass)
findPathToDeclared(resolved, declaredType, ArrayList(chain))
} else null
if (superClassChain != null) return superClassChain
for (iface in startingClass?.genericInterfaces ?: emptyArray()) {
val resolved = TypeResolver().where(startingClass!!.asParameterizedType(), startingType.asParameterizedType()).resolveType(iface)
return findPathToDeclared(resolved, declaredType, ArrayList(chain)) ?: continue
}
return null
}
/** /**
* Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types * Lookup and manufacture a serializer for the given AMQP type descriptor, assuming we also have the necessary types
* contained in the [Schema]. * contained in the [Schema].
@ -93,7 +155,8 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
} }
/** /**
* TODO: Add docs * Register a custom serializer for any type that cannot be serialized or deserialized by the default serializer
* that expects to find getters and a constructor with a parameter for each property.
*/ */
fun register(customSerializer: CustomSerializer<out Any>) { fun register(customSerializer: CustomSerializer<out Any>) {
if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) { if (!serializersByDescriptor.containsKey(customSerializer.typeDescriptor)) {
@ -118,25 +181,10 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
} }
} }
private fun restrictedTypeForName(name: String): Type {
return if (name.endsWith("[]")) {
val elementType = restrictedTypeForName(name.substring(0, name.lastIndex - 1))
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw NotSerializableException("Not able to deserialize array type: $name")
}
} else {
DeserializedParameterizedType.make(name)
}
}
private fun processRestrictedType(typeNotation: RestrictedType) { private fun processRestrictedType(typeNotation: RestrictedType) {
serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
// TODO: class loader logic, and compare the schema. // TODO: class loader logic, and compare the schema.
val type = restrictedTypeForName(typeNotation.name) val type = typeForName(typeNotation.name)
get(null, type) get(null, type)
} }
} }
@ -144,63 +192,61 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private fun processCompositeType(typeNotation: CompositeType) { private fun processCompositeType(typeNotation: CompositeType) {
serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) { serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
// TODO: class loader logic, and compare the schema. // TODO: class loader logic, and compare the schema.
val clazz = Class.forName(typeNotation.name) val type = typeForName(typeNotation.name)
get(clazz, clazz) get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
} }
} }
private fun checkParameterisedTypesConcrete(actualTypeArguments: Array<out Type>) { private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> {
for (type in actualTypeArguments) { return serializersByType.computeIfAbsent(type) {
// Needs to be another parameterised type or a class, or any type. if (isPrimitive(clazz)) {
if (type !is Class<*>) { AMQPPrimitiveSerializer(clazz)
if (type is ParameterizedType) { } else {
checkParameterisedTypesConcrete(type.actualTypeArguments) findCustomSerializer(clazz, declaredType) ?: run {
} else if (type != AnyType) { if (type.isArray()) {
throw NotSerializableException("Declared parameterised types containing $type as a parameter are not supported.") whitelisted(type.componentType())
ArraySerializer(type, this)
} else if (clazz.kotlin.objectInstance != null) {
whitelisted(clazz)
SingletonSerializer(clazz, clazz.kotlin.objectInstance!!, this)
} else {
whitelisted(type)
ObjectSerializer(type, this)
}
} }
} }
} }
} }
private fun makeClassSerializer(clazz: Class<*>): AMQPSerializer<Any> { internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
return serializersByType.computeIfAbsent(clazz) { // e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is AbstractMap, only Map.
if (isPrimitive(clazz)) { // Otherwise it needs to inject additional schema for a RestrictedType source of the super type. Could be done, but do we need it?
AMQPPrimitiveSerializer(clazz)
} else {
findCustomSerializer(clazz) ?: {
if (clazz.isArray) {
whitelisted(clazz.componentType)
ArraySerializer(clazz, this)
} else {
whitelisted(clazz)
ObjectSerializer(clazz, this)
}
}()
}
}
}
internal fun findCustomSerializer(clazz: Class<*>): AMQPSerializer<Any>? {
for (customSerializer in customSerializers) { for (customSerializer in customSerializers) {
if (customSerializer.isSerializerFor(clazz)) { if (customSerializer.isSerializerFor(clazz)) {
return customSerializer val declaredSuperClass = declaredType.asClass()?.superclass
if (declaredSuperClass == null || !customSerializer.isSerializerFor(declaredSuperClass)) {
return customSerializer
} else {
// Make a subclass serializer for the subclass and return that...
@Suppress("UNCHECKED_CAST")
return CustomSerializer.SubClass<Any>(clazz, customSerializer as CustomSerializer<Any>)
}
} }
} }
return null return null
} }
private fun whitelisted(clazz: Class<*>): Boolean { private fun whitelisted(type: Type) {
if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) { val clazz = type.asClass()!!
return true if (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) {
} else { throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.")
throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.")
} }
} }
// Recursively check the class, interfaces and superclasses for our annotation. // Recursively check the class, interfaces and superclasses for our annotation.
internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean { internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java) || return type.isAnnotationPresent(CordaSerializable::class.java) ||
type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationInHierarchy(it) } type.interfaces.any { hasAnnotationInHierarchy(it) }
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass)) || (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
} }
@ -211,9 +257,16 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
} }
companion object { companion object {
fun isPrimitive(type: Type): Boolean = type is Class<*> && Primitives.wrap(type) in primitiveTypeNames fun isPrimitive(type: Type): Boolean = primitiveTypeName(type) != null
fun primitiveTypeName(type: Type): String? = primitiveTypeNames[type as? Class<*>] fun primitiveTypeName(type: Type): String? {
val clazz = type as? Class<*> ?: return null
return primitiveTypeNames[Primitives.unwrap(clazz)]
}
fun primitiveType(type: String): Class<*>? {
return namesOfPrimitiveTypes[type]
}
private val primitiveTypeNames: Map<Class<*>, String> = mapOf( private val primitiveTypeNames: Map<Class<*>, String> = mapOf(
Boolean::class.java to "boolean", Boolean::class.java to "boolean",
@ -221,7 +274,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
UnsignedByte::class.java to "ubyte", UnsignedByte::class.java to "ubyte",
Short::class.java to "short", Short::class.java to "short",
UnsignedShort::class.java to "ushort", UnsignedShort::class.java to "ushort",
Integer::class.java to "int", Int::class.java to "int",
UnsignedInteger::class.java to "uint", UnsignedInteger::class.java to "uint",
Long::class.java to "long", Long::class.java to "long",
UnsignedLong::class.java to "ulong", UnsignedLong::class.java to "ulong",
@ -233,9 +286,36 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
Char::class.java to "char", Char::class.java to "char",
Date::class.java to "timestamp", Date::class.java to "timestamp",
UUID::class.java to "uuid", UUID::class.java to "uuid",
Binary::class.java to "binary", ByteArray::class.java to "binary",
String::class.java to "string", String::class.java to "string",
Symbol::class.java to "symbol") Symbol::class.java to "symbol")
private val namesOfPrimitiveTypes: Map<String, Class<*>> = primitiveTypeNames.map { it.value to it.key }.toMap()
fun nameForType(type: Type): String {
if (type is Class<*>) {
return primitiveTypeName(type) ?: if (type.isArray) "${nameForType(type.componentType)}[]" else type.name
} else if (type is ParameterizedType) {
return "${nameForType(type.rawType)}<${type.actualTypeArguments.joinToString { nameForType(it) }}>"
} else if (type is GenericArrayType) {
return "${nameForType(type.genericComponentType)}[]"
} else throw NotSerializableException("Unable to render type $type to a string.")
}
private fun typeForName(name: String): Type {
return if (name.endsWith("[]")) {
val elementType = typeForName(name.substring(0, name.lastIndex - 1))
if (elementType is ParameterizedType || elementType is GenericArrayType) {
DeserializedGenericArrayType(elementType)
} else if (elementType is Class<*>) {
java.lang.reflect.Array.newInstance(elementType, 0).javaClass
} else {
throw NotSerializableException("Not able to deserialize array type: $name")
}
} else {
DeserializedParameterizedType.make(name)
}
}
} }
object AnyType : WildcardType { object AnyType : WildcardType {
@ -246,4 +326,3 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
override fun toString(): String = "?" override fun toString(): String = "?"
} }
} }

View File

@ -0,0 +1,32 @@
package net.corda.core.serialization.amqp
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
/**
* A custom serializer that transports nothing on the wire (except a boolean "false", since AMQP does not support
* absolutely nothing, or null as a described type) when we have a singleton within the node that we just
* want converting back to that singleton instance on the receiving JVM.
*/
class SingletonSerializer(override val type: Class<*>, val singleton: Any, factory: SerializerFactory) : AMQPSerializer<Any> {
override val typeDescriptor = "$DESCRIPTOR_DOMAIN:${fingerprintForType(type, factory)}"
private val interfaces = interfacesForSerialization(type)
private fun generateProvides(): List<String> = interfaces.map { it.typeName }
internal val typeNotation: TypeNotation = RestrictedType(type.typeName, "Singleton", generateProvides(), "boolean", Descriptor(typeDescriptor, null), emptyList())
override fun writeClassInfo(output: SerializationOutput) {
output.writeTypeNotations(typeNotation)
}
override fun writeObject(obj: Any, data: Data, type: Type, output: SerializationOutput) {
data.withDescribed(typeNotation.descriptor) {
data.putBoolean(false)
}
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): Any {
return singleton
}
}

View File

@ -0,0 +1,11 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.serialization.amqp.CustomSerializer
import java.math.BigDecimal
/**
* A serializer for [BigDecimal], utilising the string based helper. [BigDecimal] seems to have no import/export
* features that are precision independent other than via a string. The format of the string is discussed in the
* documentation for [BigDecimal.toString].
*/
object BigDecimalSerializer : CustomSerializer.ToString<BigDecimal>(BigDecimal::class.java)

View File

@ -0,0 +1,12 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.serialization.amqp.CustomSerializer
import java.util.*
/**
* A custom serializer for the [Currency] class, utilizing the currency code string representation.
*/
object CurrencySerializer : CustomSerializer.ToString<Currency>(Currency::class.java,
withInheritance = false,
maker = { Currency.getInstance(it) },
unmaker = { it.currencyCode })

View File

@ -0,0 +1,18 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.serialization.amqp.CustomSerializer
import net.corda.core.serialization.amqp.SerializerFactory
import java.time.Instant
/**
* A serializer for [Instant] that uses a proxy object to write out the seconds since the epoch and the nanos.
*/
class InstantSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Instant, InstantSerializer.InstantProxy>(Instant::class.java, InstantProxy::class.java, factory) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
override fun toProxy(obj: Instant): InstantProxy = InstantProxy(obj.epochSecond, obj.nano)
override fun fromProxy(proxy: InstantProxy): Instant = Instant.ofEpochSecond(proxy.epochSeconds, proxy.nanos.toLong())
data class InstantProxy(val epochSeconds: Long, val nanos: Int)
}

View File

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

View File

@ -4,8 +4,8 @@ import net.corda.core.serialization.amqp.CustomSerializer
import net.corda.core.serialization.amqp.SerializerFactory import net.corda.core.serialization.amqp.SerializerFactory
import net.corda.core.serialization.amqp.constructorForDeserialization import net.corda.core.serialization.amqp.constructorForDeserialization
import net.corda.core.serialization.amqp.propertiesForSerialization import net.corda.core.serialization.amqp.propertiesForSerialization
import net.corda.core.utilities.CordaRuntimeException import net.corda.core.CordaRuntimeException
import net.corda.core.utilities.CordaThrowable import net.corda.core.CordaThrowable
import java.io.NotSerializableException import java.io.NotSerializableException
class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) { class ThrowableSerializer(factory: SerializerFactory) : CustomSerializer.Proxy<Throwable, ThrowableSerializer.ThrowableProxy>(Throwable::class.java, ThrowableProxy::class.java, factory) {

View File

@ -0,0 +1,25 @@
package net.corda.core.serialization.amqp.custom
import net.corda.core.serialization.amqp.*
import org.apache.qpid.proton.codec.Data
import org.bouncycastle.asn1.ASN1InputStream
import org.bouncycastle.asn1.x500.X500Name
import java.lang.reflect.Type
/**
* Custom serializer for X500 names that utilizes their ASN.1 encoding on the wire.
*/
object X500NameSerializer : CustomSerializer.Implements<X500Name>(X500Name::class.java) {
override val additionalSerializers: Iterable<CustomSerializer<out Any>> = emptyList()
override val schemaForDocumentation = Schema(listOf(RestrictedType(type.toString(), "", listOf(type.toString()), SerializerFactory.primitiveTypeName(ByteArray::class.java)!!, descriptor, emptyList())))
override fun writeDescribedObject(obj: X500Name, data: Data, type: Type, output: SerializationOutput) {
output.writeObject(obj.encoded, data, clazz)
}
override fun readObject(obj: Any, schema: Schema, input: DeserializationInput): X500Name {
val binary = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
return X500Name.getInstance(ASN1InputStream(binary).readObject())
}
}

View File

@ -1,10 +1,13 @@
package net.corda.carpenter package net.corda.core.serialization.carpenter
import org.objectweb.asm.ClassWriter import org.objectweb.asm.ClassWriter
import org.objectweb.asm.MethodVisitor import org.objectweb.asm.MethodVisitor
import org.objectweb.asm.Opcodes.* import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type import org.objectweb.asm.Type
import java.lang.Character.*
import java.lang.Character.isJavaIdentifierPart
import java.lang.Character.isJavaIdentifierStart
import java.util.* import java.util.*
/** /**
@ -16,6 +19,7 @@ interface SimpleFieldAccess {
operator fun get(name: String): Any? operator fun get(name: String): Any?
} }
/** /**
* A class carpenter generates JVM bytecodes for a class given a schema and then loads it into a sub-classloader. * A class carpenter generates JVM bytecodes for a class given a schema and then loads it into a sub-classloader.
* The generated classes have getters, a toString method and implement a simple property access interface. The * The generated classes have getters, a toString method and implement a simple property access interface. The
@ -61,30 +65,146 @@ interface SimpleFieldAccess {
* Equals/hashCode methods are not yet supported. * Equals/hashCode methods are not yet supported.
*/ */
class ClassCarpenter { class ClassCarpenter {
// TODO: Array types.
// TODO: Generics. // TODO: Generics.
// TODO: Sandbox the generated code when a security manager is in use. // TODO: Sandbox the generated code when a security manager is in use.
// TODO: Generate equals/hashCode. // TODO: Generate equals/hashCode.
// TODO: Support annotations. // TODO: Support annotations.
// TODO: isFoo getter patterns for booleans (this is what Kotlin generates) // TODO: isFoo getter patterns for booleans (this is what Kotlin generates)
class DuplicateNameException : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.")
class InterfaceMismatchException(msg: String) : RuntimeException(msg)
class NullablePrimitiveException(msg: String) : RuntimeException(msg)
abstract class Field(val field: Class<out Any?>) {
companion object {
const val unsetName = "Unset"
}
var name: String = unsetName
abstract val nullabilityAnnotation: String
val descriptor: String
get() = Type.getDescriptor(this.field)
val type: String
get() = if (this.field.isPrimitive) this.descriptor else "Ljava/lang/Object;"
fun generateField(cw: ClassWriter) {
val fieldVisitor = cw.visitField(ACC_PROTECTED + ACC_FINAL, name, descriptor, null, null)
fieldVisitor.visitAnnotation(nullabilityAnnotation, true).visitEnd()
fieldVisitor.visitEnd()
}
fun addNullabilityAnnotation(mv: MethodVisitor) {
mv.visitAnnotation(nullabilityAnnotation, true).visitEnd()
}
fun visitParameter(mv: MethodVisitor, idx: Int) {
with(mv) {
visitParameter(name, 0)
if (!field.isPrimitive) {
visitParameterAnnotation(idx, nullabilityAnnotation, true).visitEnd()
}
}
}
abstract fun copy(name: String, field: Class<out Any?>): Field
abstract fun nullTest(mv: MethodVisitor, slot: Int)
}
class NonNullableField(field: Class<out Any?>) : Field(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nonnull;"
constructor(name: String, field: Class<out Any?>) : this(field) {
this.name = name
}
override fun copy(name: String, field: Class<out Any?>) = NonNullableField(name, field)
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
if (!field.isPrimitive) {
with(mv) {
visitVarInsn(ALOAD, 0) // load this
visitVarInsn(ALOAD, slot) // load parameter
visitLdcInsn("param \"$name\" cannot be null")
visitMethodInsn(INVOKESTATIC,
"java/util/Objects",
"requireNonNull",
"(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object;", false)
visitInsn(POP)
}
}
}
}
class NullableField(field: Class<out Any?>) : Field(field) {
override val nullabilityAnnotation = "Ljavax/annotation/Nullable;"
constructor(name: String, field: Class<out Any?>) : this(field) {
if (field.isPrimitive) {
throw NullablePrimitiveException (
"Field $name is primitive type ${Type.getDescriptor(field)} and thus cannot be nullable")
}
this.name = name
}
override fun copy(name: String, field: Class<out Any?>) = NullableField(name, field)
override fun nullTest(mv: MethodVisitor, slot: Int) {
assert(name != unsetName)
}
}
/** /**
* A Schema represents a desired class. * A Schema represents a desired class.
*/ */
class Schema(val name: String, fields: Map<String, Class<out Any?>>, val superclass: Schema? = null, val interfaces: List<Class<*>> = emptyList()) { abstract class Schema(
val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't. val name: String,
val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap() fields: Map<String, Field>,
val superclass: Schema? = null,
val interfaces: List<Class<*>> = emptyList())
{
private fun Map<String, ClassCarpenter.Field>.descriptors() =
LinkedHashMap(this.mapValues { it.value.descriptor })
fun fieldsIncludingSuperclasses(): Map<String, Class<out Any?>> = (superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields) /* Fix the order up front if the user didn't, inject the name into the field as it's
fun descriptorsIncludingSuperclasses(): Map<String, String> = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors) neater when iterating */
val fields = LinkedHashMap(fields.mapValues { it.value.copy(it.key, it.value.field) })
fun fieldsIncludingSuperclasses(): Map<String, Field> =
(superclass?.fieldsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(fields)
fun descriptorsIncludingSuperclasses(): Map<String, String> =
(superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + fields.descriptors()
val jvmName: String
get() = name.replace(".", "/")
} }
class DuplicateName : RuntimeException("An attempt was made to register two classes with the same name within the same ClassCarpenter namespace.") private val String.jvm: String get() = replace(".", "/")
class InterfaceMismatch(msg: String) : RuntimeException(msg)
class ClassSchema(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema(name, fields, superclass, interfaces)
class InterfaceSchema(
name: String,
fields: Map<String, Field>,
superclass: Schema? = null,
interfaces: List<Class<*>> = emptyList()
) : Schema(name, fields, superclass, interfaces)
private class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) { private class CarpenterClassLoader : ClassLoader(Thread.currentThread().contextClassLoader) {
fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size) fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size)
} }
private val classloader = CarpenterClassLoader() private val classloader = CarpenterClassLoader()
private val _loaded = HashMap<String, Class<*>>() private val _loaded = HashMap<String, Class<*>>()
@ -92,8 +212,6 @@ class ClassCarpenter {
/** Returns a snapshot of the currently loaded classes as a map of full class name (package names+dots) -> class object */ /** Returns a snapshot of the currently loaded classes as a map of full class name (package names+dots) -> class object */
val loaded: Map<String, Class<*>> = HashMap(_loaded) val loaded: Map<String, Class<*>> = HashMap(_loaded)
private val String.jvm: String get() = replace(".", "/")
/** /**
* Generate bytecode for the given schema and load into the JVM. The returned class object can be used to * Generate bytecode for the given schema and load into the JVM. The returned class object can be used to
* construct instances of the generated class. * construct instances of the generated class.
@ -111,39 +229,67 @@ class ClassCarpenter {
hierarchy += cursor hierarchy += cursor
cursor = cursor.superclass cursor = cursor.superclass
} }
hierarchy.reversed().forEach { generateClass(it) }
hierarchy.reversed().forEach {
when (it) {
is InterfaceSchema -> generateInterface(it)
is ClassSchema -> generateClass(it)
}
}
return _loaded[schema.name]!! return _loaded[schema.name]!!
} }
private fun generateClass(schema: Schema): Class<*> { private fun generateInterface(interfaceSchema: Schema): Class<*> {
val jvmName = schema.name.jvm return generate(interfaceSchema) { cw, schema ->
val interfaces = schema.interfaces.map { it.name.jvm }.toTypedArray()
with(cw) {
visit(V1_8, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, schema.jvmName, null, "java/lang/Object", interfaces)
generateAbstractGetters(schema)
visitEnd()
}
}
}
private fun generateClass(classSchema: Schema): Class<*> {
return generate(classSchema) { cw, schema ->
val superName = schema.superclass?.jvmName ?: "java/lang/Object"
val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm }
with(cw) {
visit(V1_8, ACC_PUBLIC + ACC_SUPER, schema.jvmName, null, superName, interfaces)
generateFields(schema)
generateConstructor(schema)
generateGetters(schema)
if (schema.superclass == null)
generateGetMethod() // From SimplePropertyAccess
generateToString(schema)
visitEnd()
}
}
}
private fun generate(schema: Schema, generator: (ClassWriter, Schema) -> Unit): Class<*> {
// Lazy: we could compute max locals/max stack ourselves, it'd be faster. // Lazy: we could compute max locals/max stack ourselves, it'd be faster.
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS) val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
with(cw) {
// public class Name implements SimpleFieldAccess { generator(cw, schema)
val superName = schema.superclass?.name?.jvm ?: "java/lang/Object"
val interfaces = arrayOf(SimpleFieldAccess::class.java.name.jvm) + schema.interfaces.map { it.name.jvm }
visit(52, ACC_PUBLIC + ACC_SUPER, jvmName, null, superName, interfaces)
generateFields(schema)
generateConstructor(jvmName, schema)
generateGetters(jvmName, schema)
if (schema.superclass == null)
generateGetMethod() // From SimplePropertyAccess
generateToString(jvmName, schema)
visitEnd()
}
val clazz = classloader.load(schema.name, cw.toByteArray()) val clazz = classloader.load(schema.name, cw.toByteArray())
_loaded[schema.name] = clazz _loaded[schema.name] = clazz
return clazz return clazz
} }
private fun ClassWriter.generateFields(schema: Schema) { private fun ClassWriter.generateFields(schema: Schema) {
for ((name, desc) in schema.descriptors) { schema.fields.forEach { it.value.generateField(this) }
visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd()
}
} }
private fun ClassWriter.generateToString(jvmName: String, schema: Schema) { private fun ClassWriter.generateToString(schema: Schema) {
val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper" val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper"
with(visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", "", null)) { with(visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", "", null)) {
visitCode() visitCode()
@ -151,12 +297,11 @@ class ClassCarpenter {
visitLdcInsn(schema.name.split('.').last()) visitLdcInsn(schema.name.split('.').last())
visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper", "(Ljava/lang/String;)L$toStringHelper;", false) visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper", "(Ljava/lang/String;)L$toStringHelper;", false)
// Call the add() methods. // Call the add() methods.
for ((name, type) in schema.fieldsIncludingSuperclasses().entries) { for ((name, field) in schema.fieldsIncludingSuperclasses().entries) {
visitLdcInsn(name) visitLdcInsn(name)
visitVarInsn(ALOAD, 0) // this visitVarInsn(ALOAD, 0) // this
visitFieldInsn(GETFIELD, jvmName, name, schema.descriptorsIncludingSuperclasses()[name]) visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name])
val desc = if (type.isPrimitive) schema.descriptors[name] else "Ljava/lang/Object;" visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;${field.type})L$toStringHelper;", false)
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;$desc)L$toStringHelper;", false)
} }
// call toString() on the builder and return. // call toString() on the builder and return.
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()Ljava/lang/String;", false) visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "toString", "()Ljava/lang/String;", false)
@ -182,15 +327,16 @@ class ClassCarpenter {
} }
} }
private fun ClassWriter.generateGetters(jvmName: String, schema: Schema) { private fun ClassWriter.generateGetters(schema: Schema) {
for ((name, type) in schema.fields) { for ((name, type) in schema.fields) {
val descriptor = schema.descriptors[name] with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null)) {
with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + descriptor, null, null)) { type.addNullabilityAnnotation(this)
visitCode() visitCode()
visitVarInsn(ALOAD, 0) // Load 'this' visitVarInsn(ALOAD, 0) // Load 'this'
visitFieldInsn(GETFIELD, jvmName, name, descriptor) visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
when (type) { when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitInsn(IRETURN) java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
java.lang.Character.TYPE -> visitInsn(IRETURN)
java.lang.Long.TYPE -> visitInsn(LRETURN) java.lang.Long.TYPE -> visitInsn(LRETURN)
java.lang.Double.TYPE -> visitInsn(DRETURN) java.lang.Double.TYPE -> visitInsn(DRETURN)
java.lang.Float.TYPE -> visitInsn(FRETURN) java.lang.Float.TYPE -> visitInsn(FRETURN)
@ -202,9 +348,29 @@ class ClassCarpenter {
} }
} }
private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) { private fun ClassWriter.generateAbstractGetters(schema: Schema) {
with(visitMethod(ACC_PUBLIC, "<init>", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) { for ((name, field) in schema.fields) {
val opcodes = ACC_ABSTRACT + ACC_PUBLIC
with(visitMethod(opcodes, "get" + name.capitalize(), "()${field.descriptor}", null, null)) {
// abstract method doesn't have any implementation so just end
visitEnd()
}
}
}
private fun ClassWriter.generateConstructor(schema: Schema) {
with(visitMethod(
ACC_PUBLIC,
"<init>",
"(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V",
null,
null))
{
var idx = 0
schema.fields.values.forEach { it.visitParameter(this, idx++) }
visitCode() visitCode()
// Calculate the super call. // Calculate the super call.
val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap() val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap()
visitVarInsn(ALOAD, 0) visitVarInsn(ALOAD, 0)
@ -217,14 +383,15 @@ class ClassCarpenter {
val superDesc = schema.superclass.descriptorsIncludingSuperclasses().values.joinToString("") val superDesc = schema.superclass.descriptorsIncludingSuperclasses().values.joinToString("")
visitMethodInsn(INVOKESPECIAL, schema.superclass.name.jvm, "<init>", "($superDesc)V", false) visitMethodInsn(INVOKESPECIAL, schema.superclass.name.jvm, "<init>", "($superDesc)V", false)
} }
// Assign the fields from parameters. // Assign the fields from parameters.
var slot = 1 + superclassFields.size var slot = 1 + superclassFields.size
for ((name, type) in schema.fields.entries) { for ((name, field) in schema.fields.entries) {
if (type.isArray) field.nullTest(this, slot)
throw UnsupportedOperationException("Array types are not implemented yet")
visitVarInsn(ALOAD, 0) // Load 'this' onto the stack visitVarInsn(ALOAD, 0) // Load 'this' onto the stack
slot += load(slot, type) // Load the contents of the parameter onto the stack. slot += load(slot, field) // Load the contents of the parameter onto the stack.
visitFieldInsn(PUTFIELD, jvmName, name, schema.descriptors[name]) visitFieldInsn(PUTFIELD, schema.jvmName, name, field.descriptor)
} }
visitInsn(RETURN) visitInsn(RETURN)
visitMaxs(0, 0) visitMaxs(0, 0)
@ -232,23 +399,23 @@ class ClassCarpenter {
} }
} }
// Returns how many slots the given type takes up. private fun MethodVisitor.load(slot: Int, type: Field): Int {
private fun MethodVisitor.load(slot: Int, type: Class<out Any?>): Int { when (type.field) {
when (type) { java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitVarInsn(ILOAD, slot) java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot)
java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot) java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot)
java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot) java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot)
java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot) java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot)
else -> visitVarInsn(ALOAD, slot) else -> visitVarInsn(ALOAD, slot)
} }
return when (type) { return when (type.field) {
java.lang.Long.TYPE, java.lang.Double.TYPE -> 2 java.lang.Long.TYPE, java.lang.Double.TYPE -> 2
else -> 1 else -> 1
} }
} }
private fun validateSchema(schema: Schema) { private fun validateSchema(schema: Schema) {
if (schema.name in _loaded) throw DuplicateName() if (schema.name in _loaded) throw DuplicateNameException()
fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart) fun isJavaName(n: String) = n.isNotBlank() && isJavaIdentifierStart(n.first()) && n.all(::isJavaIdentifierPart)
require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" } require(isJavaName(schema.name.split(".").last())) { "Not a valid Java name: ${schema.name}" }
schema.fields.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } } schema.fields.keys.forEach { require(isJavaName(it)) { "Not a valid Java name: $it" } }
@ -257,13 +424,18 @@ class ClassCarpenter {
// actually called, which is a bit too dynamic for my tastes. // actually called, which is a bit too dynamic for my tastes.
val allFields = schema.fieldsIncludingSuperclasses() val allFields = schema.fieldsIncludingSuperclasses()
for (itf in schema.interfaces) { for (itf in schema.interfaces) {
for (method in itf.methods) { itf.methods.forEach {
val fieldNameFromItf = when { val fieldNameFromItf = when {
method.name.startsWith("get") -> method.name.substring(3).decapitalize() it.name.startsWith("get") -> it.name.substring(3).decapitalize()
else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}") else -> throw InterfaceMismatchException(
"Requested interfaces must consist only of methods that start "
+ "with 'get': ${itf.name}.${it.name}")
} }
if (fieldNameFromItf !in allFields)
throw InterfaceMismatch("Interface ${itf.name} requires a field named ${fieldNameFromItf} but that isn't found in the schema or any superclass schemas") if ((schema is ClassSchema) and (fieldNameFromItf !in allFields))
throw InterfaceMismatchException(
"Interface ${itf.name} requires a field named $fieldNameFromItf but that "
+ "isn't found in the schema or any superclass schemas")
} }
} }
} }

View File

@ -3,9 +3,11 @@ package net.corda.core.transactions
import net.corda.core.contracts.AttachmentResolutionException import net.corda.core.contracts.AttachmentResolutionException
import net.corda.core.contracts.NamedByHash import net.corda.core.contracts.NamedByHash
import net.corda.core.contracts.TransactionResolutionException import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.DigitalSignature import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.node.ServiceHub import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.SerializedBytes import net.corda.core.serialization.SerializedBytes
@ -136,17 +138,46 @@ data class SignedTransaction(val txBits: SerializedBytes<WireTransaction>,
operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList) operator fun plus(sigList: Collection<DigitalSignature.WithKey>) = withAdditionalSignatures(sigList)
/** /**
* Calls [verifySignatures] to check all required signatures are present, and then calls * Checks the transaction's signatures are valid, optionally calls [verifySignatures] to check
* [WireTransaction.toLedgerTransaction] with the passed in [ServiceHub] to resolve the dependencies, * all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
* returning an unverified LedgerTransaction. * with the passed in [ServiceHub] to resolve the dependencies, returning an unverified
* LedgerTransaction.
*
* This allows us to perform validation over the entirety of the transaction's contents.
* WireTransaction only contains StateRef for the inputs and hashes for the attachments,
* rather than ContractState instances for the inputs and Attachment instances for the attachments.
* *
* @throws AttachmentResolutionException if a required attachment was not found in storage. * @throws AttachmentResolutionException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage. * @throws TransactionResolutionException if an input points to a transaction not found in storage.
* @throws SignatureException if any signatures were invalid or unrecognised * @throws SignatureException if any signatures were invalid or unrecognised
* @throws SignaturesMissingException if any signatures that should have been present are missing. * @throws SignaturesMissingException if any signatures that should have been present are missing.
*/ */
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class) @JvmOverloads
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services) @Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub, checkSufficientSignatures: Boolean = true): LedgerTransaction {
checkSignaturesAreValid()
if (checkSufficientSignatures) verifySignatures()
return tx.toLedgerTransaction(services)
}
/**
* Checks the transaction's signatures are valid, optionally calls [verifySignatures] to check
* all required signatures are present, calls [WireTransaction.toLedgerTransaction] with the
* passed in [ServiceHub] to resolve the dependencies and return an unverified
* LedgerTransaction, then verifies the LedgerTransaction.
*
* @throws AttachmentResolutionException if a required attachment was not found in storage.
* @throws TransactionResolutionException if an input points to a transaction not found in storage.
* @throws SignatureException if any signatures were invalid or unrecognised
* @throws SignaturesMissingException if any signatures that should have been present are missing.
*/
@JvmOverloads
@Throws(SignatureException::class, AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub, checkSufficientSignatures: Boolean = true) {
checkSignaturesAreValid()
if (checkSufficientSignatures) verifySignatures()
tx.toLedgerTransaction(services).verify()
}
override fun toString(): String = "${javaClass.simpleName}(id=$id)" override fun toString(): String = "${javaClass.simpleName}(id=$id)"
} }

View File

@ -3,11 +3,13 @@ package net.corda.core.transactions
import co.paralleluniverse.strands.Strand import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.* import net.corda.core.crypto.*
import net.corda.core.internal.FlowStateMachine
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import java.security.KeyPair import java.security.KeyPair
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException
import java.time.Duration import java.time.Duration
import java.time.Instant import java.time.Instant
import java.util.* import java.util.*
@ -37,46 +39,22 @@ open class TransactionBuilder(
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(), protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command> = arrayListOf(), protected val commands: MutableList<Command> = arrayListOf(),
protected val signers: MutableSet<PublicKey> = mutableSetOf(), protected val signers: MutableSet<PublicKey> = mutableSetOf(),
protected var timeWindow: TimeWindow? = null) { protected var window: TimeWindow? = null) {
constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID()) constructor(type: TransactionType, notary: Party) : this(type, notary, (Strand.currentStrand() as? FlowStateMachine<*>)?.id?.uuid ?: UUID.randomUUID())
val time: TimeWindow? get() = timeWindow // TODO: rename using a more descriptive name (i.e. timeWindowGetter) or remove if unused.
/** /**
* Creates a copy of the builder. * Creates a copy of the builder.
*/ */
fun copy(): TransactionBuilder = fun copy() = TransactionBuilder(
TransactionBuilder( type = type,
type = type, notary = notary,
notary = notary, inputs = ArrayList(inputs),
inputs = ArrayList(inputs), attachments = ArrayList(attachments),
attachments = ArrayList(attachments), outputs = ArrayList(outputs),
outputs = ArrayList(outputs), commands = ArrayList(commands),
commands = ArrayList(commands), signers = LinkedHashSet(signers),
signers = LinkedHashSet(signers), window = window
timeWindow = timeWindow )
)
/**
* Places a [TimeWindow] in this transaction, removing any existing command if there is one.
* The command requires a signature from the Notary service, which acts as a Timestamp Authority.
* The signature can be obtained using [NotaryFlow].
*
* The window of time in which the final time-window may lie is defined as [time] +/- [timeTolerance].
* If you want a non-symmetrical time window you must add the command via [addCommand] yourself. The tolerance
* should be chosen such that your code can finish building the transaction and sending it to the TSA within that
* window of time, taking into account factors such as network latency. Transactions being built by a group of
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node.
*/
fun addTimeWindow(time: Instant, timeTolerance: Duration) = addTimeWindow(TimeWindow.withTolerance(time, timeTolerance))
fun addTimeWindow(timeWindow: TimeWindow) {
check(notary != null) { "Only notarised transactions can have a time-window" }
signers.add(notary!!.owningKey)
check(currentSigs.isEmpty()) { "Cannot change time-window after signing" }
this.timeWindow = timeWindow
}
// DOCSTART 1 // DOCSTART 1
/** A more convenient way to add items to this transaction that calls the add* methods for you based on type */ /** A more convenient way to add items to this transaction that calls the add* methods for you based on type */
@ -84,10 +62,12 @@ open class TransactionBuilder(
for (t in items) { for (t in items) {
when (t) { when (t) {
is StateAndRef<*> -> addInputState(t) is StateAndRef<*> -> addInputState(t)
is SecureHash -> addAttachment(t)
is TransactionState<*> -> addOutputState(t) is TransactionState<*> -> addOutputState(t)
is ContractState -> addOutputState(t) is ContractState -> addOutputState(t)
is Command -> addCommand(t) is Command -> addCommand(t)
is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.") is CommandData -> throw IllegalArgumentException("You passed an instance of CommandData, but that lacks the pubkey. You need to wrap it in a Command object first.")
is TimeWindow -> setTimeWindow(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}") else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
} }
} }
@ -95,49 +75,101 @@ open class TransactionBuilder(
} }
// DOCEND 1 // DOCEND 1
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, window)
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class)
fun toLedgerTransaction(services: ServiceHub) = toWireTransaction().toLedgerTransaction(services)
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, TransactionVerificationException::class)
fun verify(services: ServiceHub) {
toLedgerTransaction(services).verify()
}
open fun addInputState(stateAndRef: StateAndRef<*>): TransactionBuilder {
val notary = stateAndRef.state.notary
require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." }
signers.add(notary.owningKey)
inputs.add(stateAndRef.ref)
return this
}
fun addAttachment(attachmentId: SecureHash): TransactionBuilder {
attachments.add(attachmentId)
return this
}
fun addOutputState(state: TransactionState<*>): TransactionBuilder {
outputs.add(state)
return this
}
@JvmOverloads
fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance))
/** A default notary must be specified during builder construction to use this method */
fun addOutputState(state: ContractState): TransactionBuilder {
checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" }
addOutputState(state, notary!!)
return this
}
fun addCommand(arg: Command): TransactionBuilder {
// TODO: replace pubkeys in commands with 'pointers' to keys in signers
signers.addAll(arg.signers)
commands.add(arg)
return this
}
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
/**
* Sets the [TimeWindow] for this transaction, replacing the existing [TimeWindow] if there is one. To be valid, the
* transaction must then be signed by the notary service within this window of time. In this way, the notary acts as
* the Timestamp Authority.
*/
fun setTimeWindow(timeWindow: TimeWindow): TransactionBuilder {
check(notary != null) { "Only notarised transactions can have a time-window" }
signers.add(notary!!.owningKey)
window = timeWindow
return this
}
/**
* The [TimeWindow] for the transaction can also be defined as [time] +/- [timeTolerance]. The tolerance should be
* chosen such that your code can finish building the transaction and sending it to the Timestamp Authority within
* that window of time, taking into account factors such as network latency. Transactions being built by a group of
* collaborating parties may therefore require a higher time tolerance than a transaction being built by a single
* node.
*/
fun setTimeWindow(time: Instant, timeTolerance: Duration) = setTimeWindow(TimeWindow.withTolerance(time, timeTolerance))
// Accessors that yield immutable snapshots.
fun inputStates(): List<StateRef> = ArrayList(inputs)
fun attachments(): List<SecureHash> = ArrayList(attachments)
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
fun commands(): List<Command> = ArrayList(commands)
/** The signatures that have been collected so far - might be incomplete! */ /** The signatures that have been collected so far - might be incomplete! */
@Deprecated("Signatures should be gathered on a SignedTransaction instead.")
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>() protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
@Deprecated("Use ServiceHub.signInitialTransaction() instead.") @Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun signWith(key: KeyPair): TransactionBuilder { fun signWith(key: KeyPair): TransactionBuilder {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
val data = toWireTransaction().id val data = toWireTransaction().id
addSignatureUnchecked(key.sign(data.bytes)) addSignatureUnchecked(key.sign(data.bytes))
return this return this
} }
/**
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then
* adds it.
*
* @throws SignatureException if the signature didn't match the transaction contents.
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
checkSignature(sig)
addSignatureUnchecked(sig)
}
/**
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
*
* @throws SignatureException if the signature didn't match the transaction contents.
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
fun checkSignature(sig: DigitalSignature.WithKey) {
require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" }
sig.verify(toWireTransaction().id)
}
/** Adds the signature directly to the transaction, without checking it for validity. */ /** Adds the signature directly to the transaction, without checking it for validity. */
@Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun addSignatureUnchecked(sig: DigitalSignature.WithKey): TransactionBuilder { fun addSignatureUnchecked(sig: DigitalSignature.WithKey): TransactionBuilder {
currentSigs.add(sig) currentSigs.add(sig)
return this return this
} }
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments), @Deprecated("Use ServiceHub.signInitialTransaction() instead.")
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow)
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction { fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) { if (checkSufficientSignatures) {
val gotKeys = currentSigs.map { it.by }.toSet() val gotKeys = currentSigs.map { it.by }.toSet()
@ -149,48 +181,28 @@ open class TransactionBuilder(
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs)) return SignedTransaction(wtx.serialize(), ArrayList(currentSigs))
} }
open fun addInputState(stateAndRef: StateAndRef<*>) { /**
check(currentSigs.isEmpty()) * Checks that the given signature matches one of the commands and that it is a correct signature over the tx, then
val notary = stateAndRef.state.notary * adds it.
require(notary == this.notary) { "Input state requires notary \"$notary\" which does not match the transaction notary \"${this.notary}\"." } *
signers.add(notary.owningKey) * @throws SignatureException if the signature didn't match the transaction contents.
inputs.add(stateAndRef.ref) * @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
@Deprecated("Use WireTransaction.checkSignature() instead.")
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
checkSignature(sig)
addSignatureUnchecked(sig)
} }
fun addAttachment(attachmentId: SecureHash) { /**
check(currentSigs.isEmpty()) * Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
attachments.add(attachmentId) *
* @throws SignatureException if the signature didn't match the transaction contents.
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
@Deprecated("Use WireTransaction.checkSignature() instead.")
fun checkSignature(sig: DigitalSignature.WithKey) {
require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" }
sig.verify(toWireTransaction().id)
} }
}
fun addOutputState(state: TransactionState<*>): Int {
check(currentSigs.isEmpty())
outputs.add(state)
return outputs.size - 1
}
@JvmOverloads
fun addOutputState(state: ContractState, notary: Party, encumbrance: Int? = null) = addOutputState(TransactionState(state, notary, encumbrance))
/** A default notary must be specified during builder construction to use this method */
fun addOutputState(state: ContractState): Int {
checkNotNull(notary) { "Need to specify a notary for the state, or set a default one on TransactionBuilder initialisation" }
return addOutputState(state, notary!!)
}
fun addCommand(arg: Command) {
check(currentSigs.isEmpty())
// TODO: replace pubkeys in commands with 'pointers' to keys in signers
signers.addAll(arg.signers)
commands.add(arg)
}
fun addCommand(data: CommandData, vararg keys: PublicKey) = addCommand(Command(data, listOf(*keys)))
fun addCommand(data: CommandData, keys: List<PublicKey>) = addCommand(Command(data, keys))
// Accessors that yield immutable snapshots.
fun inputStates(): List<StateRef> = ArrayList(inputs)
fun outputStates(): List<TransactionState<*>> = ArrayList(outputs)
fun commands(): List<Command> = ArrayList(commands)
fun attachments(): List<SecureHash> = ArrayList(attachments)
}

View File

@ -2,8 +2,10 @@ package net.corda.core.transactions
import com.esotericsoftware.kryo.pool.KryoPool import com.esotericsoftware.kryo.pool.KryoPool
import net.corda.core.contracts.* import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.MerkleTree import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.keys
import net.corda.core.identity.Party import net.corda.core.identity.Party
import net.corda.core.indexOfOrThrow import net.corda.core.indexOfOrThrow
import net.corda.core.node.ServicesForResolution import net.corda.core.node.ServicesForResolution
@ -13,6 +15,7 @@ import net.corda.core.serialization.p2PKryo
import net.corda.core.serialization.serialize import net.corda.core.serialization.serialize
import net.corda.core.utilities.Emoji import net.corda.core.utilities.Emoji
import java.security.PublicKey import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate import java.util.function.Predicate
/** /**
@ -73,7 +76,7 @@ class WireTransaction(
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction { fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
return toLedgerTransaction( return toLedgerTransaction(
resolveIdentity = { services.identityService.partyFromKey(it) }, resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.storageService.attachments.openAttachment(it) }, resolveAttachment = { services.attachments.openAttachment(it) },
resolveStateRef = { services.loadState(it) } resolveStateRef = { services.loadState(it) }
) )
} }
@ -135,6 +138,17 @@ class WireTransaction(
) )
} }
/**
* Checks that the given signature matches one of the commands and that it is a correct signature over the tx.
*
* @throws SignatureException if the signature didn't match the transaction contents.
* @throws IllegalArgumentException if the signature key doesn't appear in any command.
*/
fun checkSignature(sig: DigitalSignature.WithKey) {
require(commands.any { it.signers.any { sig.by in it.keys } }) { "Signature key doesn't match any command" }
sig.verify(id)
}
override fun toString(): String { override fun toString(): String {
val buf = StringBuilder() val buf = StringBuilder()
buf.appendln("Transaction:") buf.appendln("Transaction:")

View File

@ -1,6 +1,9 @@
package net.corda.core.serialization @file:JvmName("ByteArrays")
package net.corda.core.utilities
import com.google.common.io.BaseEncoding import com.google.common.io.BaseEncoding
import net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.util.* import java.util.*
@ -11,12 +14,13 @@ import java.util.*
*/ */
@CordaSerializable @CordaSerializable
open class OpaqueBytes(val bytes: ByteArray) { open class OpaqueBytes(val bytes: ByteArray) {
init { companion object {
check(bytes.isNotEmpty()) @JvmStatic
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
} }
companion object { init {
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b)) check(bytes.isNotEmpty())
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

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