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
*.swp
*.swn
*.swo
# Files you may find useful to have in your working directory.
PLAN

2
.idea/compiler.xml generated
View File

@ -91,6 +91,8 @@
<module name="simm-valuation-demo_test" target="1.8" />
<module name="smoke-test-utils_main" target="1.8" />
<module name="smoke-test-utils_test" target="1.8" />
<module name="test-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_main" 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.
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 {
// For sharing constants between builds
Properties constants = new Properties()
file("$projectDir/constants.properties").withInputStream { constants.load(it) }
// 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
// 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
@ -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.
// Version 1.0.2 of this plugin uses capsule:1.0.1
id "us.kirchmeier.capsule" version "1.0.2"
id "com.jfrog.artifactory" version "4.4.18"
}
ext {
@ -85,6 +85,7 @@ apply plugin: 'project-report'
apply plugin: 'com.github.ben-manes.versions'
apply plugin: 'net.corda.plugins.publish-utils'
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
// 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
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) {
options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" << "-Xlint:-options" << "-parameters"
}
@ -254,7 +249,7 @@ bintrayConfig {
projectUrl = 'https://github.com/corda/corda'
gpgSign = true
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 {
name = 'Apache-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
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: 'kotlin'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
dependencies {
compile project(':core')

View File

@ -10,13 +10,14 @@ import com.fasterxml.jackson.module.kotlin.KotlinModule
import net.corda.contracts.BusinessCalendar
import net.corda.core.contracts.Amount
import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.node.NodeInfo
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.serialize
import net.i2p.crypto.eddsa.EdDSAPublicKey

View File

@ -1,6 +1,7 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
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.getOrThrow
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.messaging.startFlow
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
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.serialization.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ALICE
import net.corda.core.utilities.BOB
import net.corda.core.utilities.CHARLIE
import net.corda.core.utilities.DUMMY_NOTARY
import net.corda.testing.ALICE
import net.corda.testing.BOB
import net.corda.testing.CHARLIE
import net.corda.testing.DUMMY_NOTARY
import net.corda.flows.CashExitFlow
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
@ -113,11 +113,13 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue works end to end`() {
val anonymous = false
rpc.startFlow(::CashIssueFlow,
Amount(100, USD),
OpaqueBytes(ByteArray(1, { 1 })),
aliceNode.legalIdentity,
notaryNode.notaryIdentity
notaryNode.notaryIdentity,
anonymous
)
vaultUpdates.expectEvents(isStrict = false) {
@ -138,8 +140,9 @@ class NodeMonitorModelTest : DriverBasedTest() {
@Test
fun `cash issue and move`() {
rpc.startFlow(::CashIssueFlow, 100.DOLLARS, OpaqueBytes.of(1), aliceNode.legalIdentity, notaryNode.notaryIdentity).returnValue.getOrThrow()
rpc.startFlow(::CashPaymentFlow, 100.DOLLARS, bobNode.legalIdentity).returnValue.getOrThrow()
val anonymous = false
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 moveSmId: StateMachineRunId? = null

View File

@ -1,18 +1,18 @@
package net.corda.client.jfx.model
import com.google.common.net.HostAndPort
import javafx.beans.property.SimpleObjectProperty
import net.corda.client.rpc.CordaRPCClient
import net.corda.client.rpc.CordaRPCClientConfiguration
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.CordaRPCOps
import net.corda.core.messaging.StateMachineInfo
import net.corda.core.messaging.StateMachineTransactionMapping
import net.corda.core.messaging.StateMachineUpdate
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.seconds
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.NetworkHostAndPort
import rx.Observable
import rx.subjects.PublishSubject
@ -51,7 +51,7 @@ class NodeMonitorModel {
* Register for updates to/from a given vault.
* TODO provide an unsubscribe mechanism
*/
fun register(nodeHostAndPort: HostAndPort, username: String, password: String) {
fun register(nodeHostAndPort: NetworkHostAndPort, username: String, password: String) {
val client = CordaRPCClient(
hostAndPort = nodeHostAndPort,
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.map
import net.corda.client.jfx.utils.recordAsAssociation
import net.corda.core.ErrorOr
import net.corda.core.flows.FlowInitiator
import net.corda.core.flows.StateMachineRunId
import net.corda.core.messaging.StateMachineUpdate
import net.corda.core.utilities.Try
import org.fxmisc.easybind.EasyBind
data class ProgressStatus(val status: String?)
sealed class 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(
@ -33,11 +33,11 @@ data class Counter(
var progress: SimpleIntegerProperty = SimpleIntegerProperty(0)
) {
fun addSmm() { progress.value += 1 }
fun removeSmm(result: ErrorOr<*>) {
fun removeSmm(result: Try<*>) {
progress.value -= 1
when (result.error) {
null -> success.value += 1
else -> errored.value += 1
when (result) {
is Try.Success -> success.value += 1
is Try.Failure -> errored.value += 1
}
}
}

View File

@ -1,6 +1,7 @@
apply plugin: 'kotlin'
apply plugin: 'net.corda.plugins.quasar-utils'
apply plugin: 'net.corda.plugins.publish-utils'
apply plugin: 'com.jfrog.artifactory'
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.USD
import net.corda.core.identity.Party
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import net.corda.flows.CashFlowCommand
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 ->
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 ->
@ -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 ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
}
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 ->
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient)
CashFlowCommand.PayCash(Amount(amountIssued, currency), recipient, anonymous = true)
}
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(

View File

@ -1,7 +1,7 @@
package net.corda.client.mock
import net.corda.client.mock.Generator.Companion.choice
import net.corda.core.ErrorOr
import net.corda.core.utilities.Try
import java.util.*
/**
@ -12,7 +12,7 @@ import java.util.*
* [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.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.
*
* 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.
*/
class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
class Generator<out A>(val generate: (SplittableRandom) -> Try<A>) {
// Functor
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) } } } } }))))))
// Monad
fun <B> bind(function: (A) -> Generator<B>) =
Generator { generate(it).bind { a -> function(a).generate(it) } }
fun <B> flatMap(function: (A) -> Generator<B>): Generator<B> {
return Generator { random -> generate(random).flatMap { function(it).generate(random) } }
}
companion object {
fun <A> pure(value: A) = Generator { ErrorOr(value) }
fun <A> impure(valueClosure: () -> A) = Generator { ErrorOr(valueClosure()) }
fun <A> fail(error: Exception) = Generator<A> { ErrorOr.of(error) }
fun <A> pure(value: A) = Generator { Try.Success(value) }
fun <A> impure(valueClosure: () -> A) = Generator { Try.Success(valueClosure()) }
fun <A> fail(error: Exception) = Generator<A> { Try.Failure(error) }
// 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> {
val ranges = mutableListOf<Pair<Double, Double>>()
var current = 0.0
@ -74,11 +75,11 @@ class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
ranges.add(Pair(current, next))
current = next
}
return doubleRange(0.0, current).bind { value ->
generators[ranges.binarySearch { range ->
if (value < range.first) {
return doubleRange(0.0, current).flatMap { value ->
generators[ranges.binarySearch { (first, second) ->
if (value < first) {
1
} else if (value < range.second) {
} else if (value < second) {
0
} else {
-1
@ -91,14 +92,12 @@ class Generator<out A>(val generate: (SplittableRandom) -> ErrorOr<A>) {
val result = mutableListOf<A>()
for (generator in generators) {
val element = generator.generate(it)
val v = element.value
if (v != null) {
result.add(v)
} else {
return@Generator ErrorOr.of(element.error!!)
when (element) {
is Try.Success -> result.add(element.value)
is Try.Failure -> return@Generator element
}
}
ErrorOr(result)
Try.Success(result)
}
}
}
@ -109,11 +108,9 @@ fun <A> Generator<A>.generateOrFail(random: SplittableRandom, numberOfTries: Int
var error: Throwable? = null
for (i in 0..numberOfTries - 1) {
val result = generate(random)
val v = result.value
if (v != null) {
return v
} else {
error = result.error
error = when (result) {
is Try.Success -> return result.value
is Try.Failure -> result.exception
}
}
if (error == null) {
@ -147,9 +144,9 @@ fun Generator.Companion.doubleRange(from: Double, to: Double): Generator<Double>
fun Generator.Companion.char() = Generator {
val codePoint = Math.abs(it.nextInt()) % (17 * (1 shl 16))
if (Character.isValidCodePoint(codePoint)) {
return@Generator ErrorOr(codePoint.toChar())
return@Generator Try.Success(codePoint.toChar())
} 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>()
var finish = false
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) {
generator.generate(it).map { result.add(it) }
} else {
finish = true
ErrorOr(Unit)
Try.Success(Unit)
}
}
val e = errorOr.error
if (e != null) {
return@Generator ErrorOr.of(e)
if (result is Try.Failure) {
return@Generator result
}
}
ErrorOr(result)
Try.Success(result)
}
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)
}
}
ErrorOr(resultList)
Try.Success(resultList)
}
fun <A> Generator.Companion.sampleBernoulli(maxRatio: Double = 1.0, vararg collection: A) =

View File

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

View File

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

View File

@ -5,9 +5,9 @@ import net.corda.core.flows.FlowInitiator
import net.corda.core.getOrThrow
import net.corda.core.messaging.*
import net.corda.core.node.services.ServiceInfo
import net.corda.core.random63BitValue
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.ALICE
import net.corda.core.crypto.random63BitValue
import net.corda.core.utilities.OpaqueBytes
import net.corda.testing.ALICE
import net.corda.flows.CashException
import net.corda.flows.CashIssueFlow
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.Output
import com.esotericsoftware.kryo.pool.KryoPool
import com.google.common.net.HostAndPort
import com.google.common.util.concurrent.Futures
import net.corda.client.rpc.internal.RPCClient
import net.corda.client.rpc.internal.RPCClientConfiguration
import net.corda.core.*
import net.corda.core.crypto.random63BitValue
import net.corda.core.future
import net.corda.core.getOrThrow
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.nodeapi.RPCApi
import net.corda.nodeapi.RPCKryo
import net.corda.testing.*
import net.corda.testing.driver.poll
import org.apache.activemq.artemis.api.core.SimpleString
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
@ -24,7 +29,10 @@ import rx.Observable
import rx.subjects.PublishSubject
import rx.subjects.UnicastSubject
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
class RPCStabilityTests {
@ -77,9 +85,9 @@ class RPCStabilityTests {
val executor = Executors.newScheduledThreadPool(1)
fun startAndStop() {
rpcDriver {
ErrorOr.catch { startRpcClient<RPCOps>(HostAndPort.fromString("localhost:9999")).get() }
Try.on { startRpcClient<RPCOps>(NetworkHostAndPort("localhost", 9999)).get() }
val server = startRpcServer<RPCOps>(ops = DummyOps)
ErrorOr.catch { startRpcClient<RPCOps>(
Try.on { startRpcClient<RPCOps>(
server.get().broker.hostAndPort!!,
configuration = RPCClientConfiguration.default.copy(minimumServerProtocolVersion = 1)
).get() }

View File

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

View File

@ -1,11 +1,11 @@
package net.corda.client.rpc.internal
import com.google.common.net.HostAndPort
import net.corda.core.logElapsedTime
import net.corda.core.messaging.RPCOps
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.utilities.NetworkHostAndPort
import net.corda.core.utilities.loggerFor
import net.corda.nodeapi.ArtemisTcpTransport.Companion.tcpTransport
import net.corda.nodeapi.ConnectionDirection
@ -88,7 +88,7 @@ class RPCClient<I : RPCOps>(
val rpcConfiguration: RPCClientConfiguration = RPCClientConfiguration.default
) {
constructor(
hostAndPort: HostAndPort,
hostAndPort: NetworkHostAndPort,
sslConfiguration: SSLConfiguration? = null,
configuration: RPCClientConfiguration = RPCClientConfiguration.default
) : 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.ThreadFactoryBuilder
import net.corda.core.ThreadBox
import net.corda.core.crypto.random63BitValue
import net.corda.core.getOrThrow
import net.corda.core.messaging.RPCOps
import net.corda.core.random63BitValue
import net.corda.core.serialization.KryoPoolWithContext
import net.corda.core.utilities.*
import net.corda.nodeapi.*
@ -229,14 +229,15 @@ class RPCClientProxyHandler(
if (replyFuture == null) {
log.error("RPC reply arrived to unknown RPC ID ${serverToClient.id}, this indicates an internal RPC error.")
} else {
val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
serverToClient.result.match(
onError = {
if (rpcCallSite != null) addRpcCallSiteToThrowable(it, rpcCallSite)
replyFuture.setException(it)
},
onValue = { replyFuture.set(it) }
)
val result = serverToClient.result
when (result) {
is Try.Success -> replyFuture.set(result.value)
is Try.Failure -> {
val rpcCallSite = callSiteMap?.get(serverToClient.id.toLong)
if (rpcCallSite != null) addRpcCallSiteToThrowable(result.exception, rpcCallSite)
replyFuture.setException(result.exception)
}
}
}
}
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.crypto.SecureHash
import net.corda.core.getOrThrow
import net.corda.core.identity.Party
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.PageSpecification
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.node.services.vault.*
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.utilities.DUMMY_NOTARY
import net.corda.core.utilities.loggerFor
import net.corda.flows.CashIssueFlow
import net.corda.flows.CashPaymentFlow
@ -28,6 +24,7 @@ import net.corda.nodeapi.User
import net.corda.smoketesting.NodeConfig
import net.corda.smoketesting.NodeProcess
import org.apache.commons.io.output.NullOutputStream
import org.bouncycastle.asn1.x500.X500Name
import org.junit.After
import org.junit.Before
import org.junit.Test
@ -51,10 +48,10 @@ class StandaloneCordaRPClientTest {
private lateinit var notary: NodeProcess
private lateinit var rpcProxy: CordaRPCOps
private lateinit var connection: CordaRPCConnection
private lateinit var notaryIdentity: Party
private lateinit var notaryNode: NodeInfo
private val notaryConfig = NodeConfig(
party = DUMMY_NOTARY,
legalName = X500Name("CN=Notary Service,O=R3,OU=corda,L=Zurich,C=CH"),
p2pPort = port.andIncrement,
rpcPort = port.andIncrement,
webPort = port.andIncrement,
@ -67,7 +64,7 @@ class StandaloneCordaRPClientTest {
notary = NodeProcess.Factory().create(notaryConfig)
connection = notary.connect()
rpcProxy = connection.proxy
notaryIdentity = fetchNotaryIdentity()
notaryNode = fetchNotaryIdentity()
}
@After
@ -95,7 +92,7 @@ class StandaloneCordaRPClientTest {
@Test
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)
}
@ -103,7 +100,7 @@ class StandaloneCordaRPClientTest {
fun `test starting tracked flow`() {
var trackCount = 0
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 ->
log.info("Flow>> $msg")
@ -115,7 +112,7 @@ class StandaloneCordaRPClientTest {
@Test
fun `test network map`() {
assertEquals(DUMMY_NOTARY.name, notaryIdentity.name)
assertEquals(notaryConfig.legalName, notaryNode.legalIdentity.name)
}
@Test
@ -132,38 +129,15 @@ class StandaloneCordaRPClientTest {
}
// 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)
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
fun `test vault track by`() {
val (vault, vaultUpdates) = rpcProxy.vaultTrackBy<Cash.State>()
assertEquals(0, vault.totalStatesAvailable)
assertEquals(0, vault.states.size)
var updateCount = 0
vaultUpdates.subscribe { update ->
@ -172,7 +146,7 @@ class StandaloneCordaRPClientTest {
}
// 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)
assertNotEquals(0, updateCount)
@ -186,18 +160,18 @@ class StandaloneCordaRPClientTest {
@Test
fun `test vault query by`() {
// 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)
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 queryResults = rpcProxy.vaultQueryBy<Cash.State>(criteria, paging, sorting)
assertEquals(1, queryResults.totalStatesAvailable)
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)
assertEquals(3, moreResults.totalStatesAvailable) // 629 - 100 + 100
@ -209,11 +183,11 @@ class StandaloneCordaRPClientTest {
assertEquals(629.POUNDS, cashBalance[Currency.getInstance("GBP")])
}
private fun fetchNotaryIdentity(): Party {
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapUpdates()
private fun fetchNotaryIdentity(): NodeInfo {
val (nodeInfo, nodeUpdates) = rpcProxy.networkMapFeed()
nodeUpdates.notUsed()
assertEquals(1, nodeInfo.size)
return nodeInfo[0].legalIdentity
return nodeInfo[0]
}
// 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 net.corda.core.getOrThrow
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.nodeapi.RPCSinceVersion
import net.corda.testing.RPCDriverExposedDSLInterface
@ -158,12 +158,12 @@ class ClientRPCInfrastructureTests : AbstractRPCTest() {
val clientQuotes = LinkedBlockingQueue<String>()
val clientFuture = proxy.makeComplicatedListenableFuture()
clientFuture.success {
clientFuture.thenMatch({
val name = it.first
it.second.success {
it.second.thenMatch({
clientQuotes += "Quote by $name: $it"
}
}
}, {})
}, {})
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.messaging.RPCOps
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.node.services.messaging.RPCServerConfiguration
import net.corda.testing.RPCDriverExposedDSLInterface

View File

@ -13,7 +13,7 @@
<Appenders>
<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>
<!-- Required for printBasicInfo -->
@ -27,7 +27,7 @@
fileName="${sys:log-path}/${log-name}.log"
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>
<TimeBasedTriggeringPolicy/>

View File

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

View File

@ -6,6 +6,10 @@ repositories {
mavenCentral()
}
// This tracks the gradle plugins version and not Corda
version gradle_plugins_version
group 'net.corda.plugins'
dependencies {
// TypeSafe Config: for simple and human friendly config files.
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.Map;
public class CordformNode {
public class CordformNode implements NodeDefinition {
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 java.util.*

View File

@ -7,7 +7,6 @@ import com.google.common.base.Throwables
import com.google.common.io.ByteStreams
import com.google.common.util.concurrent.*
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.newSecureRandom
import net.corda.core.crypto.sha256
import net.corda.core.flows.FlowException
import net.corda.core.serialization.CordaSerializable
@ -24,9 +23,11 @@ import java.nio.file.*
import java.nio.file.attribute.FileAttribute
import java.time.Duration
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.function.BiConsumer
import java.util.stream.Stream
import java.util.zip.Deflater
import java.util.zip.ZipEntry
@ -59,12 +60,6 @@ infix fun Int.checkedAdd(b: Int) = Math.addExact(this, b)
@Suppress("unused")
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 */
fun <T> Future<T>.getOrThrow(timeout: Duration? = null): T {
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> {
override fun addListener(listener: Runnable, executor: Executor) {
base.whenCompleteAsync(BiConsumer { _, _ -> listener.run() }, executor)
}
}
fun <F : ListenableFuture<*>, V> F.then(block: (F) -> V) = addListener(Runnable { block(this) }, MoreExecutors.directExecutor())
// Some utilities for working with Guava listenable futures.
fun <T> ListenableFuture<T>.then(executor: Executor, body: () -> Unit) = addListener(Runnable(body), executor)
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 {
fun <U, V> Future<U>.match(success: (U) -> V, failure: (Throwable) -> V): V {
return success(try {
getOrThrow()
} catch (t: Throwable) {
body(t)
}
return failure(t)
})
}
infix fun <T> ListenableFuture<T>.then(body: () -> Unit): ListenableFuture<T> = apply { then(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.success(body: (T) -> Unit): ListenableFuture<T> = apply { success(RunOnCallerThread, body) }
infix fun <T> ListenableFuture<T>.failure(body: (Throwable) -> Unit): ListenableFuture<T> = apply { failure(RunOnCallerThread, body) }
fun ListenableFuture<*>.andForget(log: Logger) = failure(RunOnCallerThread) { log.error("Background task failed:", it) }
fun <U, V, W> ListenableFuture<U>.thenMatch(success: (U) -> V, failure: (Throwable) -> W) = then { it.match(success, failure) }
fun ListenableFuture<*>.andForget(log: Logger) = then { it.match({}, { log.error("Background task failed:", it) }) }
@Suppress("UNCHECKED_CAST") // We need the awkward cast because otherwise F cannot be nullable, even though it's safe.
infix fun <F, T> ListenableFuture<F>.map(mapper: (F) -> T): ListenableFuture<T> = Futures.transform(this, { (mapper as (F?) -> T)(it) })
infix fun <F, T> ListenableFuture<F>.flatMap(mapper: (F) -> ListenableFuture<T>): ListenableFuture<T> = Futures.transformAsync(this) { mapper(it!!) }
@ -121,12 +98,12 @@ inline fun <T> SettableFuture<T>.catch(block: () -> T) {
fun <A> ListenableFuture<out A>.toObservable(): Observable<A> {
return Observable.create { subscriber ->
success {
thenMatch({
subscriber.onNext(it)
subscriber.onCompleted()
} failure {
}, {
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 */
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 {
val start = System.nanoTime()
block()
@ -353,63 +327,6 @@ data class InputStreamAndHash(val inputStream: InputStream, val sha256: SecureHa
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.
* @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.Party
import net.corda.core.serialization.*
import net.corda.core.utilities.OpaqueBytes
import java.io.FileNotFoundException
import java.io.IOException
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
* 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
* list should just contain the owner.
* The participants list should normally be derived from the contents of the state.
*/
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
* 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
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
* for that contract state.
* Represents a contract state (unconsumed output) of type [LinearState] and a point in time that a lifecycle event is
* 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
* 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
/**
* This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a given point in time.
* e.g. run a fixing flow.
* This class represents the lifecycle activity that a contract state of type [LinearState] would like to perform at a
* 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
* 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
/**
* Implemented by a program that implements business logic on the shared ledger. All participants run this code for
* every [LedgerTransaction] they see on the network, for every input and output state. All contracts must accept the
* transaction for it to be accepted: failure of any aborts the entire thing. The time is taken from a trusted
* time-window attached to the transaction itself i.e. it is NOT necessarily the current time.
* every [net.corda.core.transactions.LedgerTransaction] they see on the network, for every input and output state. All
* contracts must accept the transaction for it to be accepted: failure of any aborts the entire thing. The time is taken
* from a trusted 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.
*/
@ -461,9 +462,8 @@ interface Attachment : NamedByHash {
abstract class AbstractAttachment(dataLoader: () -> ByteArray) : Attachment {
companion object {
fun SerializeAsTokenContext.attachmentDataLoader(id: SecureHash): () -> ByteArray {
val storage = serviceHub.storageService.attachments
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() }
}
}

View File

@ -1,7 +1,7 @@
package net.corda.core.contracts
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.WireTransaction
import java.util.*
@ -18,7 +18,7 @@ import java.util.concurrent.Callable
* @param transactions map of transaction id to [SignedTransaction].
* @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>> {
class Query(
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.serialization.CordaSerializable
import net.corda.core.serialization.DeserializeAsKotlinObjectDef
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import java.security.PublicKey
@ -61,7 +60,7 @@ sealed class TransactionType {
abstract fun verifyTransaction(tx: LedgerTransaction)
/** 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 */
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
* 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]
* and adds the list of participants to the signers set for every input state.
*/
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 })
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 {
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 {
if (random != null) {
initSign(privateKey, random)

View File

@ -1,24 +1,26 @@
package net.corda.core.crypto
import net.corda.core.random63BitValue
import net.i2p.crypto.eddsa.*
import net.corda.core.crypto.composite.CompositeKey
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.spec.EdDSANamedCurveSpec
import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable
import net.i2p.crypto.eddsa.spec.EdDSAPrivateKeySpec
import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec
import org.bouncycastle.asn1.ASN1EncodableVector
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.ASN1Sequence
import org.bouncycastle.asn1.DERSequence
import org.bouncycastle.asn1.*
import org.bouncycastle.asn1.bc.BCObjectIdentifiers
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
import org.bouncycastle.asn1.sec.SECObjectIdentifiers
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.BasicConstraints
import org.bouncycastle.asn1.x509.Extension
import org.bouncycastle.asn1.x509.NameConstraints
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
import org.bouncycastle.asn1.x509.*
import org.bouncycastle.asn1.x9.X9ObjectIdentifiers
import org.bouncycastle.cert.X509CertificateHolder
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.BCSphincs256PublicKey
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.security.*
import java.security.KeyFactory
import java.security.KeyPairGenerator
import java.security.spec.InvalidKeySpecException
import java.security.spec.PKCS8EncodedKeySpec
import java.security.spec.X509EncodedKeySpec
@ -80,7 +77,8 @@ object Crypto {
val RSA_SHA256 = SignatureScheme(
1,
"RSA_SHA256",
PKCSObjectIdentifiers.id_RSASSA_PSS,
AlgorithmIdentifier(PKCSObjectIdentifiers.rsaEncryption, null),
emptyList(),
BouncyCastleProvider.PROVIDER_NAME,
"RSA",
"SHA256WITHRSAANDMGF1",
@ -93,7 +91,8 @@ object Crypto {
val ECDSA_SECP256K1_SHA256 = SignatureScheme(
2,
"ECDSA_SECP256K1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256k1),
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256k1)),
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA",
"SHA256withECDSA",
@ -106,7 +105,8 @@ object Crypto {
val ECDSA_SECP256R1_SHA256 = SignatureScheme(
3,
"ECDSA_SECP256R1_SHA256",
X9ObjectIdentifiers.ecdsa_with_SHA256,
AlgorithmIdentifier(X9ObjectIdentifiers.ecdsa_with_SHA256, SECObjectIdentifiers.secp256r1),
listOf(AlgorithmIdentifier(X9ObjectIdentifiers.id_ecPublicKey, SECObjectIdentifiers.secp256r1)),
BouncyCastleProvider.PROVIDER_NAME,
"ECDSA",
"SHA256withECDSA",
@ -119,10 +119,12 @@ object Crypto {
val EDDSA_ED25519_SHA512 = SignatureScheme(
4,
"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.
BouncyCastleProvider.PROVIDER_NAME,
EdDSAKey.KEY_ALGORITHM,
"1.3.101.112",
EdDSAEngine.SIGNATURE_ALGORITHM,
EdDSANamedCurveTable.getByName("ED25519"),
256,
@ -133,10 +135,12 @@ object Crypto {
* 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.
*/
val SHA512_256 = DLSequence(arrayOf(NISTObjectIdentifiers.id_sha512_256))
val SPHINCS256_SHA256 = SignatureScheme(
5,
"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",
"SPHINCS256",
"SHA512WITHSPHINCS256",
@ -146,6 +150,22 @@ object Crypto {
"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). */
val DEFAULT_SIGNATURE_SCHEME = EDDSA_ED25519_SHA512
@ -158,12 +178,18 @@ object Crypto {
ECDSA_SECP256K1_SHA256,
ECDSA_SECP256R1_SHA256,
EDDSA_ED25519_SHA512,
SPHINCS256_SHA256
SPHINCS256_SHA256,
COMPOSITE_KEY
).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.
private val algorithmGroups = supportedSignatureSchemes.values.groupBy { it.algorithmName }
/**
* Map of X.509 algorithm identifiers to signature schemes Corda recognises. See RFC 2459 for the format of
* 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
// that could cause unexpected and suspicious behaviour.
@ -171,17 +197,34 @@ object Crypto {
// The val is private to avoid any harmful state changes.
private val providerMap: Map<String, Provider> = mapOf(
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.
private fun getBouncyCastleProvider() = BouncyCastleProvider().apply {
putAll(EdDSASecurityProvider())
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID, KeyInfoConverter(EDDSA_ED25519_SHA512))
addKeyInfoConverter(EDDSA_ED25519_SHA512.signatureOID.algorithm, KeyInfoConverter(EDDSA_ED25519_SHA512))
}
init {
// 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.
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.
* @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")
/**
@ -202,10 +246,24 @@ object Crypto {
* @return a currently supported SignatureScheme.
* @throws IllegalArgumentException if the requested key type is not supported.
*/
fun findSignatureScheme(key: Key): SignatureScheme {
val algorithm = matchingAlgorithmName(key.algorithm)
algorithmGroups[algorithm]?.filter { validateKey(it, key) }?.firstOrNull { return it }
throw IllegalArgumentException("Unsupported key algorithm: ${key.algorithm} or invalid key format")
@Throws(IllegalArgumentException::class)
fun findSignatureScheme(key: PublicKey): SignatureScheme {
val keyInfo = SubjectPublicKeyInfo.getInstance(key.encoded)
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)
fun decodePrivateKey(encodedKey: ByteArray): PrivateKey {
val algorithm = matchingAlgorithmName(PKCS8Key.parseKey(DerValue(encodedKey)).algorithm)
// There are cases where the same key algorithm is applied to different signature schemes.
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
// 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.")
val keyInfo = PrivateKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(keyInfo.privateKeyAlgorithm)
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePrivate(PKCS8EncodedKeySpec(encodedKey))
}
/**
@ -270,19 +318,9 @@ object Crypto {
*/
@Throws(IllegalArgumentException::class)
fun decodePublicKey(encodedKey: ByteArray): PublicKey {
val algorithm = matchingAlgorithmName(X509Key.parse(DerValue(encodedKey)).algorithm)
// There are cases where the same key algorithm is applied to different signature schemes.
// Currently, this occurs with ECDSA as it applies to either secp256K1 or secp256R1 curves.
// 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.")
val subjectPublicKeyInfo = SubjectPublicKeyInfo.getInstance(encodedKey)
val signatureScheme = findSignatureScheme(subjectPublicKeyInfo.algorithm)
return KeyFactory.getInstance(signatureScheme.algorithmName, providerMap[signatureScheme.providerName]).generatePublic(X509EncodedKeySpec(encodedKey))
}
/**
@ -527,7 +565,7 @@ object Crypto {
if (signatureScheme.algSpec != null)
keyPairGenerator.initialize(signatureScheme.algSpec, newSecureRandom())
else
keyPairGenerator.initialize(signatureScheme.keySize, newSecureRandom())
keyPairGenerator.initialize(signatureScheme.keySize!!, newSecureRandom())
return keyPairGenerator.generateKeyPair()
}
@ -834,16 +872,6 @@ object Crypto {
/** Check if the requested [SignatureScheme] is supported by the system. */
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.
private fun validateKey(signatureScheme: SignatureScheme, key: Key): Boolean {
return when (key) {

View File

@ -2,42 +2,12 @@
package net.corda.core.crypto
import net.corda.core.identity.AnonymousParty
import net.corda.core.identity.Party
import net.corda.core.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.utilities.OpaqueBytes
import java.math.BigInteger
import net.corda.core.utilities.SgxSupport
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.
* @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)
fun KeyPair.sign(bytesToSign: ByteArray) = private.sign(bytesToSign, 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.
@ -262,3 +221,17 @@ private val _newSecureRandom: () -> SecureRandom by lazy {
*/
@Throws(NoSuchAlgorithmException::class)
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
import net.corda.core.identity.Party
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.PublicKey
import java.security.SignatureException
@ -46,7 +45,4 @@ open class DigitalSignature(bits: ByteArray) : OpaqueBytes(bits) {
@Throws(InvalidKeyException::class, SignatureException::class)
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).
fun parsePublicKeyBase58(base58String: String): PublicKey = base58String.base58ToByteArray().deserialize<PublicKey>()
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
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 java.security.PublicKey
import java.time.Instant

View File

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

View File

@ -1,6 +1,6 @@
package net.corda.core.crypto
import org.bouncycastle.asn1.ASN1ObjectIdentifier
import org.bouncycastle.asn1.x509.AlgorithmIdentifier
import java.security.Signature
import java.security.spec.AlgorithmParameterSpec
@ -8,7 +8,9 @@ import java.security.spec.AlgorithmParameterSpec
* This class is used to define a digital signature scheme.
* @param schemeNumberID we assign a number ID for more efficient on-wire serialisation. Please ensure uniqueness between schemes.
* @param schemeCodeName code name for this signature scheme (e.g. RSA_SHA256, ECDSA_SECP256K1_SHA256, ECDSA_SECP256R1_SHA256, EDDSA_ED25519_SHA512, SPHINCS-256_SHA512).
* @param 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 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")
@ -20,10 +22,11 @@ import java.security.spec.AlgorithmParameterSpec
data class SignatureScheme(
val schemeNumberID: Int,
val schemeCodeName: String,
val signatureOID: ASN1ObjectIdentifier,
val signatureOID: AlgorithmIdentifier,
val alternativeOIDs: List<AlgorithmIdentifier>,
val providerName: String,
val algorithmName: String,
val signatureName: String,
val algSpec: AlgorithmParameterSpec?,
val keySize: Int,
val keySize: Int?,
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 org.bouncycastle.asn1.ASN1ObjectIdentifier
@ -10,14 +10,10 @@ import java.security.spec.AlgorithmParameterSpec
/**
* Dedicated class for storing a set of signatures that comprise [CompositeKey].
*/
class CompositeSignature : Signature(ALGORITHM) {
class CompositeSignature : Signature(SIGNATURE_ALGORITHM) {
companion object {
val ALGORITHM = "2.25.30086077608615255153862931087626791003"
// UUID-based OID
// 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())
val SIGNATURE_ALGORITHM = "COMPOSITESIG"
fun getService(provider: Provider) = Provider.Service(provider, "Signature", SIGNATURE_ALGORITHM, CompositeSignature::class.java.name, emptyList(), emptyMap())
}
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
/**

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
import net.corda.core.utilities.CordaException
import net.corda.core.utilities.CordaRuntimeException
import net.corda.core.CordaException
import net.corda.core.CordaRuntimeException
// DOCSTART 1
/**

View File

@ -4,13 +4,13 @@ import co.paralleluniverse.fibers.Suspendable
import net.corda.core.crypto.SecureHash
import net.corda.core.identity.Party
import net.corda.core.internal.FlowStateMachine
import net.corda.core.messaging.DataFeed
import net.corda.core.node.ServiceHub
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.ProgressTracker
import net.corda.core.utilities.UntrustworthyData
import net.corda.core.utilities.debug
import org.slf4j.Logger
import rx.Observable
/**
* 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.
*/
@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 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
@ -215,10 +215,10 @@ abstract class FlowLogic<out T> {
*
* @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
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
fun waitForLedgerCommit(hash: SecureHash): SignedTransaction = stateMachine.waitForLedgerCommit(hash, this)
////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////
private var _stateMachine: FlowStateMachine<*>? = null
/**

View File

@ -4,8 +4,10 @@ import kotlin.annotation.AnnotationTarget.CLASS
import kotlin.reflect.KClass
/**
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The flow that
* does the initiating is specified by the [value] property and itself must be annotated with [InitiatingFlow].
* This annotation is required by any [FlowLogic] that is designed to be initiated by a counterparty flow. The class must
* 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
* 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.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
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.crypto.toBase58String
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
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.crypto.CertificateAndKeyPair
import net.corda.core.crypto.toBase58String
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 java.security.PublicKey

View File

@ -1,7 +1,6 @@
package net.corda.core.messaging
import com.google.common.util.concurrent.ListenableFuture
import net.corda.core.ErrorOr
import net.corda.core.contracts.Amount
import net.corda.core.contracts.ContractState
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.FlowLogic
import net.corda.core.flows.StateMachineRunId
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.node.NodeInfo
import net.corda.core.node.services.NetworkMapCache
import net.corda.core.node.services.StateMachineTransactionMapping
import net.corda.core.node.services.Vault
import net.corda.core.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.QueryCriteria
import net.corda.core.node.services.vault.Sort
import net.corda.core.serialization.CordaSerializable
import net.corda.core.transactions.SignedTransaction
import net.corda.core.utilities.Try
import org.bouncycastle.asn1.x500.X500Name
import rx.Observable
import java.io.InputStream
@ -32,7 +34,7 @@ data class StateMachineInfo(
val id: StateMachineRunId,
val flowLogicClassName: String,
val initiator: FlowInitiator,
val progressTrackerStepAndUpdates: Pair<String, Observable<String>>?
val progressTrackerStepAndUpdates: DataFeed<String, String>?
) {
override fun toString(): String = "${javaClass.simpleName}($id, $flowLogicClassName)"
}
@ -45,16 +47,16 @@ sealed class StateMachineUpdate {
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
* 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 {
/**
* 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
/**
* 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
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)
@ -76,11 +81,18 @@ interface CordaRPCOps : RPCOps {
* and returns a [Vault.Page] object containing the following:
* 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.
* 3. the [PageSpecification] used in the query
* 4. a total number of results available (for subsequent paging if necessary)
* 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
* 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.
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification].
* @throws VaultQueryException if the query cannot be executed for any reason
* (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
@RPCReturnsObservables
@ -119,59 +131,69 @@ interface CordaRPCOps : RPCOps {
*
* 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).
*/
*/
// DOCSTART VaultTrackByAPI
@RPCReturnsObservables
fun <T : ContractState> vaultTrackBy(criteria: QueryCriteria,
paging: PageSpecification,
sorting: Sort,
contractType: Class<out T>): Vault.PageAndUpdates<T>
contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update>
// DOCEND VaultTrackByAPI
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
// Java Helpers
// 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)
}
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)
}
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)
}
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)
}
// 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
// TODO: Remove this from the interface
@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
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
* such mappings as well.
*/
@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.
*/
@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].
@ -282,6 +304,13 @@ interface CordaRPCOps : RPCOps {
/** Enumerates the class names of the flows that this node knows about. */
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(),
@ -292,7 +321,7 @@ inline fun <reified T : ContractState> CordaRPCOps.vaultQueryBy(criteria: QueryC
inline fun <reified T : ContractState> CordaRPCOps.vaultTrackBy(criteria: QueryCriteria = QueryCriteria.VaultQueryCriteria(),
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)
}
@ -340,6 +369,27 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startFlow
arg3: D
): 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.
*/
@ -382,3 +432,18 @@ inline fun <T : Any, A, B, C, D, reified R : FlowLogic<T>> CordaRPCOps.startTrac
arg2: C,
arg3: D
): 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.PartyAndCertificate
import net.corda.core.messaging.SingleMessageRecipient
import net.corda.core.node.services.ServiceInfo
import net.corda.core.node.services.ServiceType
import net.corda.core.serialization.CordaSerializable
import org.bouncycastle.cert.X509CertificateHolder
import net.corda.core.utilities.NetworkHostAndPort
/**
* 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.
*/
// TODO We currently don't support multi-IP/multi-identity nodes, we only left slots in the data structures.
@CordaSerializable
data class NodeInfo(val address: SingleMessageRecipient,
val legalIdentityAndCert: PartyAndCertificate,
data class NodeInfo(val addresses: List<NetworkHostAndPort>,
val legalIdentityAndCert: PartyAndCertificate, //TODO This field will be removed in future PR which gets rid of services.
val legalIdentitiesAndCerts: Set<PartyAndCertificate>,
val platformVersion: Int,
var advertisedServices: List<ServiceEntry> = emptyList(),
val physicalLocation: PhysicalLocation? = null) {
val worldMapLocation: WorldMapLocation? = null) {
init {
require(advertisedServices.none { it.identity == legalIdentityAndCert }) { "Service identities must be different from node legal identity" }
}
val legalIdentity: Party
get() = legalIdentityAndCert.party
val notaryIdentity: Party
@ -35,7 +35,4 @@ data class NodeInfo(val address: SingleMessageRecipient,
fun serviceIdentities(type: ServiceType): List<Party> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity.party }
}
fun servideIdentitiesAndCert(type: ServiceType): List<PartyAndCertificate> {
return advertisedServices.filter { it.info.type.isSubTypeOf(type) }.map { it.identity }
}
}

View File

@ -43,15 +43,15 @@ data class WorldCoordinate(val latitude: Double, val longitude: Double) {
* The [countryCode] field is a two letter ISO country code.
*/
@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.
*/
object CityDatabase {
private val matcher = Regex("^([a-zA-Z- ]*) \\((..)\\)$")
private val caseInsensitiveLookups = HashMap<String, PhysicalLocation>()
val cityMap = HashMap<String, PhysicalLocation>()
private val caseInsensitiveLookups = HashMap<String, WorldMapLocation>()
val cityMap = HashMap<String, WorldMapLocation>()
init {
javaClass.getResourceAsStream("cities.txt").bufferedReader().useLines { lines ->
@ -60,7 +60,7 @@ object CityDatabase {
val (name, lng, lat) = line.split('\t')
val matchResult = matcher.matchEntire(name) ?: throw Exception("Could not parse line: $line")
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
cityMap[city] = location
}

View File

@ -1,5 +1,6 @@
package net.corda.core.node
import com.google.common.collect.Lists
import net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.node.services.*
@ -17,7 +18,9 @@ import java.time.Clock
*/
interface ServicesForResolution {
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].
@ -40,7 +43,14 @@ interface ServiceHub : ServicesForResolution {
val vaultService: VaultService
val vaultQueryService: VaultQueryService
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 transactionVerifierService: TransactionVerifierService
val clock: Clock
@ -54,41 +64,41 @@ interface ServiceHub : ServicesForResolution {
fun <T : SerializeAsToken> cordaService(type: Class<T>): T
/**
* Given a [SignedTransaction], writes it to the local storage for validated transactions and then
* sends them to the vault for further processing. Expects to be run within a database transaction.
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing. This is expected to be run within a database transaction.
*
* @param txs The transactions to record.
*/
// TODO: Make this take a single tx.
fun recordTransactions(txs: Iterable<SignedTransaction>)
/**
* Given some [SignedTransaction]s, writes them to the local storage for validated transactions and then
* sends them to the vault for further processing.
*
* @param txs The transactions to record.
* Stores the given [SignedTransaction]s in the local transaction storage and then sends them to the vault for
* further processing. This is expected to be run within a database transaction.
*/
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].
*
* @throws TransactionResolutionException if the [StateRef] points to a non-existent transaction.
* @throws TransactionResolutionException if [stateRef] points to a non-existent transaction.
*/
@Throws(TransactionResolutionException::class)
override fun loadState(stateRef: StateRef): TransactionState<*> {
val definingTx = storageService.validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
return definingTx.tx.outputs[stateRef.index]
val stx = validatedTransactions.getTransaction(stateRef.txhash) ?: throw TransactionResolutionException(stateRef.txhash)
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> {
val definingTx = storageService.validatedTransactions.getTransaction(ref.txhash) ?: throw TransactionResolutionException(ref.txhash)
return definingTx.tx.outRef<T>(ref.index)
@Throws(TransactionResolutionException::class)
fun <T : ContractState> toStateAndRef(stateRef: StateRef): StateAndRef<T> {
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.
* 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
* 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
* will be used to create the signature.
*/
@ -108,8 +118,8 @@ interface ServiceHub : ServicesForResolution {
* otherwise an IllegalArgumentException will be thrown.
* 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
* the matching [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
* the matching [java.security.PrivateKey] will be looked up internally and used to sign.
* 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.
*/
val notaryIdentityKey: PublicKey get() = this.myInfo.notaryIdentity.owningKey
@ -119,7 +129,7 @@ interface ServiceHub : ServicesForResolution {
* using keys stored inside the node.
* @param builder The [TransactionBuilder] to seal with the node's signature.
* 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
* to sign with.
* @return Returns a SignedTransaction with the new node signature attached.
@ -130,7 +140,6 @@ interface ServiceHub : ServicesForResolution {
return builder.toSignedTransaction(false)
}
/**
* Helper method to construct an initial partially signed transaction from a TransactionBuilder
* using the default identity key contained in the node.
@ -140,36 +149,35 @@ interface ServiceHub : ServicesForResolution {
*/
fun signInitialTransaction(builder: TransactionBuilder): SignedTransaction = signInitialTransaction(builder, legalIdentityKey)
/**
* Helper method to construct an initial partially signed transaction from a [TransactionBuilder]
* using a set of keys all held in this node.
* @param builder The [TransactionBuilder] to seal with the node's signature.
* 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.
* @return Returns a [SignedTransaction] with the new node signature attached.
*/
fun signInitialTransaction(builder: TransactionBuilder, signingPubKeys: Iterable<PublicKey>): SignedTransaction {
var stx: SignedTransaction? = null
for (pubKey in signingPubKeys) {
stx = if (stx == null) {
signInitialTransaction(builder, pubKey)
} else {
addSignature(stx, pubKey)
}
val it = signingPubKeys.iterator()
var stx = signInitialTransaction(builder, it.next())
while (it.hasNext()) {
stx = addSignature(stx, it.next())
}
return stx!!
return stx
}
/**
* Helper method to create an additional signature for an existing (partially) [SignedTransaction].
* @param signedTransaction The [SignedTransaction] to which the signature will apply.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing.
* @return The [DigitalSignature.WithKey] generated by signing with the internally held [PrivateKey].
* @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [net.corda.core.crypto.CompositeKey] the first leaf key found locally will be used
* 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
@ -177,16 +185,21 @@ interface ServiceHub : ServicesForResolution {
* @param signedTransaction The SignedTransaction to which the signature will apply.
* @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].
* @param signedTransaction The [SignedTransaction] to which the signature will be added.
* @param publicKey The [PublicKey] matching to a signing [PrivateKey] hosted in the node.
* If the [PublicKey] is actually a [CompositeKey] the first leaf key found locally will be used for signing.
* @param publicKey The [PublicKey] matching to a signing [java.security.PrivateKey] hosted in the node.
* 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.
*/
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]

View File

@ -2,21 +2,14 @@ package net.corda.core.node.services
import net.corda.core.contracts.Attachment
import net.corda.core.crypto.SecureHash
import java.io.IOException
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.
*/
interface AttachmentStorage {
/**
* If true, newly inserted attachments will be unzipped to a subdirectory of the [storePath]. This is intended for
* human browsing convenience: the attachment itself will still be the file (that is, edits to the extracted directory
* will not have any effect).
*/
var automaticallyExtractAttachments: Boolean
var storePath: Path
/**
* Returns a handle to a locally stored attachment, or null if it's not known. The handle can be used to open
* a stream for the data, which will be a zip/jar file.
@ -27,13 +20,14 @@ interface AttachmentStorage {
* 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.
*
* Note that you should not pass a [JarInputStream] into this method and it will throw if you do, because access
* to the raw byte stream is required.
* Note that you should not pass a [java.util.jar.JarInputStream] into this method and it will throw if you do, because
* access to the raw byte stream is required.
*
* @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(FileAlreadyExistsException::class, IOException::class)
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 net.corda.core.contracts.Contract
import net.corda.core.identity.AbstractParty
import net.corda.core.identity.Party
import net.corda.core.messaging.DataFeed
import net.corda.core.node.NodeInfo
import net.corda.core.node.ServiceHub
import net.corda.core.randomOrNull
import net.corda.core.serialization.CordaSerializable
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
* 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. */
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()
/**
* 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. */
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 com.google.common.util.concurrent.ListenableFuture
import net.corda.core.contracts.*
import net.corda.core.node.services.vault.QueryCriteria
import net.corda.core.crypto.CompositeKey
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
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.Party
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.QueryCriteria
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.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import net.corda.core.toFuture
import net.corda.core.transactions.LedgerTransaction
import net.corda.core.transactions.TransactionBuilder
import net.corda.core.transactions.WireTransaction
import org.bouncycastle.cert.X509CertificateHolder
import net.corda.flows.AnonymisedIdentity
import rx.Observable
import rx.subjects.PublishSubject
import java.io.InputStream
import java.security.PublicKey
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.time.Instant
import java.util.*
@ -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 */
fun <T : ContractState> containsType(clazz: Class<T>, status: StateStatus) =
when(status) {
when (status) {
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) }
|| 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:
* 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
* 3) the [PageSpecification] definition used to bound this result set
* 4) a total number of states that met the given [QueryCriteria]
* Note that this may be more than the specified [PageSpecification.pageSize], and should be used to perform
* further pagination (by issuing new queries).
* 3) a total number of states that met the given [QueryCriteria] if a [PageSpecification] was provided
* (otherwise defaults to -1)
* 4) Status types used in this query: UNCONSUMED, CONSUMED, ALL
* 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
data class Page<out T : ContractState>(val states: List<StateAndRef<T>>,
val statesMetadata: List<StateMetadata>,
val pageable: PageSpecification,
val totalStatesAvailable: Int,
val stateTypes: StateStatus)
val totalStatesAvailable: Long,
val stateTypes: StateStatus,
val otherResults: List<Any>)
@CordaSerializable
data class StateMetadata(val ref: StateRef,
@ -140,9 +144,6 @@ class Vault<out T : ContractState>(val states: Iterable<StateAndRef<T>>) {
val notaryKey: String,
val lockId: String?,
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
@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
@ -274,7 +275,7 @@ interface VaultService {
* Optionally may specify whether to include [StateRef] that have been marked as soft locked (default is true)
*/
// 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>>
// DOCEND VaultStatesQuery
@ -350,15 +351,18 @@ interface VaultQueryService {
* and returns a [Vault.Page] object containing the following:
* 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.
* 3. the [PageSpecification] used in the query
* 4. a total number of results available (for subsequent paging if necessary)
* 3. total number of results available if [PageSpecification] supplied (otherwise returns -1)
* 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
* (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.
* It is the responsibility of the Client to request further pages and/or specify a more suitable [PageSpecification].
* Note2: you can also annotate entity fields with JPA OrderBy annotation to achieve the same effect as explicit sorting
* 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].
*/
@Throws(VaultQueryException::class)
fun <T : ContractState> _queryBy(criteria: QueryCriteria,
@ -381,7 +385,7 @@ interface VaultQueryService {
fun <T : ContractState> _trackBy(criteria: QueryCriteria,
paging: PageSpecification,
sorting: Sort,
contractType: Class<out T>): Vault.PageAndUpdates<T>
contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update>
// DOCEND VaultQueryAPI
// Note: cannot apply @JvmOverloads to interfaces nor interface implementations
@ -402,19 +406,19 @@ interface VaultQueryService {
return _queryBy(criteria, paging, sorting, contractType)
}
fun <T : ContractState> trackBy(contractType: Class<out T>): Vault.Page<T> {
return _queryBy(QueryCriteria.VaultQueryCriteria(), PageSpecification(), Sort(emptySet()), contractType)
fun <T : ContractState> trackBy(contractType: Class<out T>): DataFeed<Vault.Page<T>, Vault.Update> {
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)
}
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)
}
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)
}
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)
}
}
@ -439,23 +443,23 @@ inline fun <reified T : ContractState> VaultQueryService.queryBy(criteria: Query
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)
}
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)
}
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)
}
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)
}
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)
}
@ -488,9 +492,17 @@ interface KeyManagementService {
* @return X.509 certificate and path to the trust root.
*/
@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 publicKey The [PublicKey] partner to an internally held [PrivateKey], either derived from the node's primary identity,
* or previously generated via the [freshKey] method.
@ -521,44 +533,6 @@ interface FileUploader {
fun accepts(type: String): Boolean
}
interface AttachmentsStorageService {
/** Provides access to storage of arbitrary JAR files (which may contain only data, no code). */
val attachments: AttachmentStorage
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.
*/

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
import net.corda.core.crypto.SecureHash
import net.corda.core.messaging.DataFeed
import net.corda.core.transactions.SignedTransaction
import rx.Observable
/**
* Thread-safe storage of transactions.
*/
interface ReadOnlyTransactionStorage {
interface TransactionStorage {
/**
* 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.
*/
fun track(): Pair<List<SignedTransaction>, Observable<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
}
fun track(): DataFeed<List<SignedTransaction>, SignedTransaction>
}

View File

@ -1,3 +1,5 @@
@file:JvmName("QueryCriteria")
package net.corda.core.node.services.vault
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.identity.AbstractParty
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.serialization.CordaSerializable
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.utilities.OpaqueBytes
import org.bouncycastle.asn1.x500.X500Name
import java.time.Instant
import java.util.*
@ -26,32 +26,36 @@ sealed class QueryCriteria {
@CordaSerializable
data class TimeCondition(val type: TimeInstantType, val predicate: ColumnPredicate<Instant>)
/**
* VaultQueryCriteria: provides query by attributes defined in [VaultSchema.VaultStates]
*/
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() {
abstract class CommonQueryCriteria : QueryCriteria() {
abstract val status: Vault.StateStatus
override fun visit(parser: IQueryCriteriaParser): Collection<Predicate> {
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]
*/
data class LinearStateQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
val linearId: List<UniqueIdentifier>? = null,
val dealRef: List<String>? = null) : QueryCriteria() {
data class LinearStateQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val linearId: List<UniqueIdentifier>? = null,
val dealRef: List<String>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
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
* [Commodity] as used in [CommodityContract] state
*/
data class FungibleAssetQueryCriteria @JvmOverloads constructor(
val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
val issuerPartyName: List<AbstractParty>? = null,
val issuerRef: List<OpaqueBytes>? = null) : QueryCriteria() {
data class FungibleAssetQueryCriteria @JvmOverloads constructor(val participants: List<AbstractParty>? = null,
val owner: List<AbstractParty>? = null,
val quantity: ColumnPredicate<Long>? = null,
val issuerPartyName: List<AbstractParty>? = null,
val issuerRef: List<OpaqueBytes>? = null,
override val status: Vault.StateStatus = Vault.StateStatus.UNCONSUMED) : CommonQueryCriteria() {
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.
*/
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> {
return parser.parseCriteria(this)
return parser.parseCriteria(this as CommonQueryCriteria).plus(parser.parseCriteria(this))
}
}
// 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> {
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> {
return parser.parseOr(this.a, this.b)
}
@ -109,9 +114,13 @@ sealed class QueryCriteria {
RECORDED,
CONSUMED
}
infix fun and(criteria: QueryCriteria): QueryCriteria = AndComposition(this, criteria)
infix fun or(criteria: QueryCriteria): QueryCriteria = OrComposition(this, criteria)
}
interface IQueryCriteriaParser {
fun parseCriteria(criteria: QueryCriteria.CommonQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.FungibleAssetQueryCriteria): Collection<Predicate>
fun parseCriteria(criteria: QueryCriteria.LinearStateQueryCriteria): 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 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
import net.corda.core.schemas.PersistentState
@ -44,11 +46,23 @@ enum class CollectionOperator {
NOT_IN
}
@CordaSerializable
enum class AggregateFunctionType {
COUNT,
AVG,
MIN,
MAX,
SUM,
}
@CordaSerializable
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 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 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
@ -65,6 +79,7 @@ sealed class 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 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> {
@ -72,9 +87,11 @@ fun <O, R> resolveEnclosingObjectFromExpression(expression: CriteriaExpression<O
is CriteriaExpression.BinaryLogical -> resolveEnclosingObjectFromExpression(expression.left)
is CriteriaExpression.Not -> resolveEnclosingObjectFromExpression(expression.expression)
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> {
return when (column) {
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:
* https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/PagingAndSortingRepository.html
*/
val DEFAULT_PAGE_NUM = 0
val DEFAULT_PAGE_SIZE = 200
const val DEFAULT_PAGE_NUM = 1
const val DEFAULT_PAGE_SIZE = 200
/**
* Note: this maximum size will be configurable in future (to allow for large JVM heap sized node configurations)
* Use [PageSpecification] to correctly handle a number of bounded pages of [MAX_PAGE_SIZE].
* Note: use [PageSpecification] to correctly handle a number of bounded pages of a pre-configured 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
* [DEFAULT_PAGE_SIZE] with a maximum page size of [MAX_PAGE_SIZE]
* [PageSpecification] allows specification of a page number (starting from [DEFAULT_PAGE_NUM]) and 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
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
@ -133,24 +154,30 @@ data class Sort(val columns: Collection<SortColumn>) {
@CordaSerializable
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 */
NOTARY_NAME("notaryName"),
CONTRACT_TYPE("contractStateClassName"),
STATE_STATUS("stateStatus"),
RECORDED_TIME("recordedTime"),
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 */
UUID("uuid"),
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 */
QUANTITY("quantity"),
ISSUER_REF("issuerRef")
@ -183,10 +210,15 @@ sealed class SortAttribute {
object Builder {
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 <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 <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?>.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
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))
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)
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)
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))
fun <R : Comparable<R>> Field.notIn(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.NOT_IN, collection))
@JvmStatic fun <R> Field.equal(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.EQUAL, value))
@JvmStatic fun <R> Field.notEqual(value: R) = predicate(ColumnPredicate.EqualityComparison(EqualityComparisonOperator.NOT_EQUAL, value))
@JvmStatic fun <R : Comparable<R>> Field.lessThan(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN, value)
@JvmStatic fun <R : Comparable<R>> Field.lessThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.LESS_THAN_OR_EQUAL, value)
@JvmStatic fun <R : Comparable<R>> Field.greaterThan(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN, value)
@JvmStatic fun <R : Comparable<R>> Field.greaterThanOrEqual(value: R) = comparePredicate(BinaryComparisonOperator.GREATER_THAN_OR_EQUAL, value)
@JvmStatic fun <R : Comparable<R>> Field.between(from: R, to: R) = predicate(ColumnPredicate.Between(from, to))
@JvmStatic fun <R : Comparable<R>> Field.`in`(collection: Collection<R>) = predicate(ColumnPredicate.CollectionExpression(CollectionOperator.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> 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 <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 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 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 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)

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.crypto.toBase58String
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 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?)
var key: String
) {
constructor(party: net.corda.core.identity.AbstractParty)
constructor(party: AbstractParty)
: 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 net.corda.core.contracts.ContractState
import net.corda.core.contracts.StateRef
import net.corda.core.serialization.toHexString
import net.corda.core.utilities.toHexString
import java.io.Serializable
import javax.persistence.Column
import javax.persistence.Embeddable

View File

@ -1,6 +1,8 @@
package net.corda.core.serialization
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.Util
import net.corda.core.node.AttachmentsClassLoader
@ -29,7 +31,11 @@ fun makeAllButBlacklistedClassResolver(): ClassResolver {
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. */
override fun getRegistration(type: Class<*>): Registration? {
return super.getRegistration(type) ?: checkClass(type)
@ -59,7 +65,7 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
return checkClass(type.superclass)
}
// It's safe to have the Class already, since Kryo loads it with initialisation off.
// 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.
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")
@ -68,16 +74,38 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
}
override fun registerImplicit(type: Class<*>): Registration {
// We have to set reference to true, since the flag influences how String fields are treated and we want it to be consistent.
val references = kryo.references
try {
kryo.references = true
return register(Registration(type, kryo.getDefaultSerializer(type), NAME.toInt()))
} finally {
kryo.references = references
val hasAnnotation = checkForAnnotation(type)
// If something is not annotated, or AMQP is disabled, we stay serializing with Kryo. This will typically be the
// case for flow checkpoints (ignoring all cases where AMQP is disabled) since our top level messaging data structures
// are annotated and once we enter AMQP serialisation we stay with it for the entire object subgraph.
if (!hasAnnotation || !amqpEnabled) {
val objectInstance = try {
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 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.
@ -85,13 +113,13 @@ class CordaClassResolver(val whitelist: ClassWhitelist) : DefaultClassResolver()
return (type.classLoader !is AttachmentsClassLoader)
&& !KryoSerializable::class.java.isAssignableFrom(type)
&& !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.
private fun hasAnnotationOnInterface(type: Class<*>): Boolean {
return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationOnInterface(it) }
|| (type.superclass != null && hasAnnotationOnInterface(type.superclass))
private fun hasInheritedAnnotation(type: Class<*>): Boolean {
return type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasInheritedAnnotation(it) }
|| (type.superclass != null && hasInheritedAnnotation(type.superclass))
}
// 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
* and was used to track down the initial set.
*
* @suppress
*/
@Suppress("unused")
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.UnmodifiableCollectionsSerializer
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.node.CordaPluginRegistry
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.pqc.jcajce.provider.sphincs.BCSphincs256PrivateKey
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.slf4j.Logger
import sun.security.provider.certpath.X509CertPath
import java.io.BufferedInputStream
import java.io.FileInputStream
import java.io.InputStream
import java.lang.reflect.Modifier.isPublic
import java.security.cert.CertPath
import java.security.cert.X509Certificate
import java.util.*
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.
fieldSerializerConfig.cachedFieldNameStrategy = FieldSerializer.CachedFieldNameStrategy.EXTENDED
// Allow construction of objects using a JVM backdoor that skips invoking the constructors, if there is no
// no-arg constructor available.
instantiatorStrategy = Kryo.DefaultInstantiatorStrategy(StdInstantiatorStrategy())
instantiatorStrategy = CustomInstantiatorStrategy()
register(Arrays.asList("").javaClass, ArraysAsListSerializer())
register(SignedTransaction::class.java, ImmutableClassSerializer(SignedTransaction::class))
@ -73,7 +73,7 @@ object DefaultKryoCustomizer {
noReferencesWithin<WireTransaction>()
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, ECPublicKeyImplSerializer)
register(EdDSAPublicKey::class.java, Ed25519PublicKeySerializer)
register(EdDSAPrivateKey::class.java, Ed25519PrivateKeySerializer)
@ -86,9 +86,6 @@ object DefaultKryoCustomizer {
// This ensures a NonEmptySetSerializer is constructed with an initial value.
register(NonEmptySet::class.java, NonEmptySetSerializer)
/** This ensures any kotlin objects that implement [DeserializeAsKotlinObjectDef] are read back in as singletons. */
addDefaultSerializer(DeserializeAsKotlinObjectDef::class.java, KotlinObjectSerializer)
addDefaultSerializer(SerializeAsToken::class.java, SerializeAsTokenSerializer<SerializeAsToken>())
register(MetaData::class.java, MetaDataSerializer)
@ -113,9 +110,22 @@ object DefaultKryoCustomizer {
register(BCRSAPublicKey::class.java, PublicKeySerializer)
register(BCSphincs256PrivateKey::class.java, PrivateKeySerializer)
register(BCSphincs256PublicKey::class.java, PublicKeySerializer)
register(sun.security.ec.ECPublicKeyImpl::class.java, PublicKeySerializer)
val customization = KryoSerializationCustomization(this)
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 net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.crypto.composite.CompositeKey
import net.corda.core.identity.Party
import net.corda.core.node.AttachmentsClassLoader
import net.corda.core.transactions.WireTransaction
import net.corda.core.utilities.LazyPool
import net.corda.core.utilities.SgxSupport
import net.corda.core.utilities.OpaqueBytes
import net.i2p.crypto.eddsa.EdDSAPrivateKey
import net.i2p.crypto.eddsa.EdDSAPublicKey
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 */
@ThreadSafe
object WireTransactionSerializer : Serializer<WireTransaction>() {
@VisibleForTesting
internal val attachmentsClassLoaderEnabled = "attachments.class.loader.enabled"
override fun write(kryo: Kryo, output: Output, obj: WireTransaction) {
kryo.writeClassAndObject(output, obj.inputs)
kryo.writeClassAndObject(output, obj.attachments)
@ -333,12 +338,12 @@ object WireTransactionSerializer : Serializer<WireTransaction>() {
}
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.
serializationContext.serviceHub.storageService.attachmentsClassLoaderEnabled || return null
val missing = ArrayList<SecureHash>()
val attachments = ArrayList<Attachment>()
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)
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.
@ThreadSafe
object CompositeKeySerializer : Serializer<CompositeKey>() {
@ -449,19 +468,6 @@ inline fun <reified T> readListOfLength(kryo: Kryo, input: Input, minLen: Int =
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.
private val internalKryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeAllButBlacklistedClassResolver())) }.build()
private val kryoPool = KryoPool.Builder { DefaultKryoCustomizer.customize(CordaKryo(makeStandardClassResolver())) }.build()
@ -519,7 +525,7 @@ inline fun <T : Any> Kryo.register(
return register(
type.java,
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)
}
)
@ -625,7 +631,7 @@ object X500NameSerializer : Serializer<X500Name>() {
*/
@ThreadSafe
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 {
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
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
import com.google.common.primitives.Primitives
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
/**
* 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> {
override val typeDescriptor: String = SerializerFactory.primitiveTypeName(Primitives.wrap(clazz))!!
override val typeDescriptor: String = SerializerFactory.primitiveTypeName(clazz)!!
override val type: Type = clazz
// 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) {
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 java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
@ -12,14 +10,10 @@ import java.lang.reflect.Type
class ArraySerializer(override val type: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
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 fun makeElementType(): Type {
return (type as? Class<*>)?.componentType ?: (type as GenericArrayType).genericComponentType
}
override fun writeClassInfo(output: SerializationOutput) {
if (output.writeTypeNotations(typeNotation)) {
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 {
val elementType: Class<*> = if (type is Class<*>) {
type
} else if (type is ParameterizedType) {
type.rawType as Class<*>
} else {
throw NotSerializableException("Unexpected array element type $type")
}
val elementType = type.asClass() ?: throw NotSerializableException("Unexpected array element type $type")
val list = this
return java.lang.reflect.Array.newInstance(elementType, this.size).apply {
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 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) {
if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,5 +1,6 @@
package net.corda.core.serialization.amqp
import net.corda.core.serialization.amqp.SerializerFactory.Companion.nameForType
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
@ -10,11 +11,16 @@ import java.lang.reflect.Type
abstract class CustomSerializer<T> : AMQPSerializer<T> {
/**
* This is a collection of custom serializers that this custom serializer depends on. e.g. for proxy objects
* that refer to arrays of types etc.
* that refer to other custom types etc.
*/
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
protected abstract val descriptor: Descriptor
/**
* 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)
/**
* 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>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = clazz == this.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 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>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = this.clazz.isAssignableFrom(clazz)
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
}
/**
* Addition base features over and above [Implements] or [Is] custom serializer for when the serialize form should be
* 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 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>() {
override fun isSerializerFor(clazz: Class<*>): Boolean = if (withInheritance) this.clazz.isAssignableFrom(clazz) else this.clazz == clazz
override val type: Type get() = clazz
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${clazz.name}"
override val typeDescriptor: String = "$DESCRIPTOR_DOMAIN:${nameForType(clazz)}"
override fun writeClassInfo(output: SerializationOutput) {}
override val descriptor: Descriptor = Descriptor(typeDescriptor)
private val proxySerializer: ObjectSerializer by lazy { ObjectSerializer(proxyClass, factory) }
override val schemaForDocumentation: Schema by lazy {
val typeNotations = mutableSetOf<TypeNotation>(CompositeType(type.typeName, null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields))
val typeNotations = mutableSetOf<TypeNotation>(CompositeType(nameForType(type), null, emptyList(), descriptor, (proxySerializer.typeNotation as CompositeType).fields))
for (additional in additionalSerializers) {
typeNotations.addAll(additional.schemaForDocumentation.types)
}
@ -102,4 +138,38 @@ abstract class CustomSerializer<T> : AMQPSerializer<T> {
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 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.UnsignedByte
import org.apache.qpid.proton.codec.Data
import java.io.NotSerializableException
import java.lang.reflect.Type
import java.nio.ByteBuffer
import java.util.*
data class objectAndEnvelope<T>(val obj: T, val envelope: Envelope)
/**
* 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
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)
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
@ -29,25 +105,18 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
*/
@Throws(NotSerializableException::class)
fun <T : Any> deserialize(bytes: SerializedBytes<T>, clazz: Class<T>): T {
try {
// 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")
}
val envelope = Envelope.get(data)
return des<T, T>(bytes, clazz) { bytes, clazz ->
var envelope = getEnvelope(bytes)
clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
}
}
@Throws(NotSerializableException::class)
internal fun <T : Any> deserializeAndReturnEnvelope(bytes: SerializedBytes<T>, clazz: Class<T>): objectAndEnvelope<T> {
return des<T, objectAndEnvelope<T>>(bytes, clazz) { bytes, clazz ->
val envelope = getEnvelope(bytes)
// Now pick out the obj and schema from the envelope.
return clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz))
} catch(nse: NotSerializableException) {
throw nse
} catch(t: Throwable) {
throw NotSerializableException("Unexpected throwable: ${t.message} ${Throwables.getStackTraceAsString(t)}")
} finally {
objectHistory.clear()
objectAndEnvelope(clazz.cast(readObjectOrNull(envelope.obj, envelope.schema, clazz)), envelope)
}
}
@ -66,25 +135,10 @@ class DeserializationInput(internal val serializerFactory: SerializerFactory = S
if (serializer.type != type && !serializer.type.isSubClassOf(type))
throw NotSerializableException("Described type with descriptor ${obj.descriptor} was expected to be of type $type")
return serializer.readObject(obj.described, schema, this)
} else if (obj is Binary) {
return obj.array
} else {
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
import com.google.common.primitives.Primitives
import java.io.NotSerializableException
import java.lang.reflect.ParameterizedType
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 {
// 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 {

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 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) {
if (output.writeTypeNotations(typeNotation)) {

View File

@ -1,5 +1,6 @@
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.codec.Data
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).
*/
class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSerializer<Any> {
class ObjectSerializer(val clazz: Type, factory: SerializerFactory) : AMQPSerializer<Any> {
override val type: Type get() = clazz
private val javaConstructor: Constructor<Any>?
internal val propertySerializers: Collection<PropertySerializer>
@ -20,7 +21,9 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe
javaConstructor = kotlinConstructor?.javaConstructor
propertySerializers = propertiesForSerialization(kotlinConstructor, clazz, factory)
}
private val typeName = clazz.name
private val typeName = nameForType(clazz)
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.
@ -65,7 +68,7 @@ class ObjectSerializer(val clazz: Class<*>, factory: SerializerFactory) : AMQPSe
}
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
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Method
import java.lang.reflect.Type
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.javaGetter
/**
* 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 writeProperty(obj: Any?, data: Data, output: SerializationOutput)
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 mandatory: Boolean = generateMandatory()
private val isInterface: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isInterface ?: false
private val isJVMPrimitive: Boolean get() = (readMethod.genericReturnType as? Class<*>)?.isPrimitive ?: false
private val isInterface: Boolean get() = resolvedType.asClass()?.isInterface ?: false
private val isJVMPrimitive: Boolean get() = resolvedType.asClass()?.isPrimitive ?: false
private fun generateType(): String {
return if (isInterface) "*" else {
val primitiveName = SerializerFactory.primitiveTypeName(readMethod.genericReturnType)
return primitiveName ?: readMethod.genericReturnType.typeName
}
return if (isInterface || resolvedType == Any::class.java) "*" else SerializerFactory.nameForType(resolvedType)
}
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? {
if (isJVMPrimitive) {
return when (readMethod.genericReturnType) {
return when (resolvedType) {
java.lang.Boolean.TYPE -> "false"
java.lang.Character.TYPE -> "&#0"
else -> "0"
@ -54,13 +53,12 @@ sealed class PropertySerializer(val name: String, val readMethod: Method) {
}
companion object {
fun make(name: String, readMethod: Method, factory: SerializerFactory): PropertySerializer {
val type = readMethod.genericReturnType
if (SerializerFactory.isPrimitive(type)) {
fun make(name: String, readMethod: Method, resolvedType: Type, factory: SerializerFactory): PropertySerializer {
if (SerializerFactory.isPrimitive(resolvedType)) {
// 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 {
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).
*/
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.
private val typeSerializer: AMQPSerializer<Any> by lazy { lazyTypeSerializer() }
private val typeSerializer: AMQPSerializer<*> by lazy { lazyTypeSerializer() }
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? {
return input.readObjectOrNull(obj, schema, readMethod.genericReturnType)
return input.readObjectOrNull(obj, schema, resolvedType)
}
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).
*/
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 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) {
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.Hashing
import net.corda.core.crypto.Base58
import net.corda.core.serialization.OpaqueBytes
import net.corda.core.crypto.toBase64
import net.corda.core.utilities.OpaqueBytes
import org.apache.qpid.proton.amqp.DescribedType
import org.apache.qpid.proton.amqp.UnsignedLong
import org.apache.qpid.proton.codec.Data
@ -12,6 +12,8 @@ import java.io.NotSerializableException
import java.lang.reflect.GenericArrayType
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.TypeVariable
import java.util.*
// TODO: get an assigned number as per AMQP spec
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 NOT_NULLABLE_HASH: String = "Nullable = false"
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.
@ -320,44 +323,83 @@ private val ANY_TYPE_HASH: String = "Any type = true"
* different.
*/
// 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) {
hasher.putUnencodedChars(ALREADY_SEEN_HASH)
} else {
alreadySeen += type
if (type is SerializerFactory.AnyType) {
hasher.putUnencodedChars(ANY_TYPE_HASH)
} else if (type is Class<*>) {
if (type.isArray) {
fingerprintForType(type.componentType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (Collection::class.java.isAssignableFrom(type) || Map::class.java.isAssignableFrom(type)) {
hasher.putUnencodedChars(type.name)
} else {
// 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
try {
if (type is SerializerFactory.AnyType) {
hasher.putUnencodedChars(ANY_TYPE_HASH)
} else if (type is Class<*>) {
if (type.isArray) {
fingerprintForType(type.componentType, contextType, alreadySeen, hasher, factory).putUnencodedChars(ARRAY_HASH)
} else if (SerializerFactory.isPrimitive(type)) {
hasher.putUnencodedChars(type.name)
} else if (isCollectionOrMap(type)) {
hasher.putUnencodedChars(type.name)
} 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) {
// Hash the rawType + params
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")
} catch(e: NotSerializableException) {
throw NotSerializableException("${e.message} -> $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 java.beans.Introspector
import java.io.NotSerializableException
import java.lang.reflect.Method
import java.lang.reflect.Modifier
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
import java.lang.reflect.*
import java.util.*
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
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
* 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)) {
var preferredCandidate: KFunction<T>? = clazz.kotlin.primaryConstructor
var preferredCandidate: KFunction<Any>? = clazz.kotlin.primaryConstructor
var annotatedCount = 0
val kotlinConstructors = clazz.kotlin.constructors
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
* names accessible via reflection.
*/
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, clazz: Class<*>, factory: SerializerFactory): Collection<PropertySerializer> {
return if (kotlinConstructor != null) propertiesForSerialization(kotlinConstructor, factory) else propertiesForSerialization(clazz, factory)
internal fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>?, type: Type, factory: SerializerFactory): Collection<PropertySerializer> {
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 <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
// 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] }
@ -78,10 +78,11 @@ private fun <T : Any> propertiesForSerialization(kotlinConstructor: KFunction<T>
// Check that the method has a getter in java.
val getter = matchingProperty.readMethod ?: throw NotSerializableException("Property has no getter method for $name of $clazz." +
" If using Java and the parameter name looks anonymous, check that you have the -parameters option specified in the Java compiler.")
val returnType = resolveTypeVariables(getter.genericReturnType, type)
if (constructorParamTakesReturnTypeOfGetter(getter, param)) {
rc += PropertySerializer.make(name, getter, factory)
rc += PropertySerializer.make(name, getter, returnType, factory)
} 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
@ -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 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.
val properties = Introspector.getBeanInfo(clazz).propertyDescriptors.filter { it.name != "class" }.sortedBy { it.name }
val rc: MutableList<PropertySerializer> = ArrayList(properties.size)
for (property in properties) {
// Check that the method has a getter in java.
val getter = property.readMethod ?: throw NotSerializableException("Property has no getter method for ${property.name} of $clazz.")
rc += PropertySerializer.make(property.name, getter, factory)
val returnType = resolveTypeVariables(getter.genericReturnType, type)
rc += PropertySerializer.make(property.name, getter, returnType, factory)
}
return rc
}
internal fun interfacesForSerialization(clazz: Class<*>): List<Type> {
internal fun interfacesForSerialization(type: Type): List<Type> {
val interfaces = LinkedHashSet<Type>()
exploreType(clazz, interfaces)
exploreType(type, interfaces)
return interfaces.toList()
}
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.isInterface) interfaces += clazz
if (clazz.isInterface) interfaces += type!!
for (newInterface in clazz.genericInterfaces) {
if (newInterface !in interfaces) {
interfaces += newInterface
exploreType(newInterface, interfaces)
exploreType(resolveTypeVariables(newInterface, type), 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()
block()
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
import com.google.common.primitives.Primitives
import com.google.common.reflect.TypeResolver
import net.corda.core.checkNotUnorderedHashMap
import net.corda.core.serialization.AllWhitelist
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.
*/
// 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: 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: maybe support for caching of serialized form of some core types for performance
// 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: schema evolution solution when the fingerprints do not line up.
// TODO: allow definition of well known types that are left out of the schema.
// TODO: automatically support byte[] without having to wrap in [Binary].
// 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
class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
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.
*
* @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).
*/
@Throws(NotSerializableException::class)
fun get(actualType: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
if (declaredType is ParameterizedType) {
return serializersByType.computeIfAbsent(declaredType) {
// We allow only Collection and Map.
val rawType = declaredType.rawType
if (rawType is Class<*>) {
checkParameterisedTypesConcrete(declaredType.actualTypeArguments)
if (Collection::class.java.isAssignableFrom(rawType)) {
CollectionSerializer(declaredType, this)
} else if (Map::class.java.isAssignableFrom(rawType)) {
makeMapSerializer(declaredType)
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
}
} else {
throw NotSerializableException("Declared types of $declaredType are not supported.")
fun get(actualClass: Class<*>?, declaredType: Type): AMQPSerializer<Any> {
val declaredClass = declaredType.asClass()
if (declaredClass != null) {
val actualType: Type = inferTypeVariables(actualClass, declaredClass, declaredType) ?: declaredType
if (Collection::class.java.isAssignableFrom(declaredClass)) {
return serializersByType.computeIfAbsent(declaredType) {
CollectionSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType), null), this)
}
} else if (Map::class.java.isAssignableFrom(declaredClass)) {
return serializersByType.computeIfAbsent(declaredClass) {
makeMapSerializer(declaredType as? ParameterizedType ?: DeserializedParameterizedType(declaredClass, arrayOf(AnyType, AnyType), null))
}
}
} 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 {
return makeClassSerializer(actualType ?: declaredType)
return makeClassSerializer(actualClass ?: declaredClass, actualType, declaredType)
}
} else if (declaredType is GenericArrayType) {
return serializersByType.computeIfAbsent(declaredType) { ArraySerializer(declaredType, this) }
} else {
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
* 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>) {
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) {
serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
// TODO: class loader logic, and compare the schema.
val type = restrictedTypeForName(typeNotation.name)
val type = typeForName(typeNotation.name)
get(null, type)
}
}
@ -144,63 +192,61 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
private fun processCompositeType(typeNotation: CompositeType) {
serializersByDescriptor.computeIfAbsent(typeNotation.descriptor.name!!) {
// TODO: class loader logic, and compare the schema.
val clazz = Class.forName(typeNotation.name)
get(clazz, clazz)
val type = typeForName(typeNotation.name)
get(type.asClass() ?: throw NotSerializableException("Unable to build composite type for $type"), type)
}
}
private fun checkParameterisedTypesConcrete(actualTypeArguments: Array<out Type>) {
for (type in actualTypeArguments) {
// Needs to be another parameterised type or a class, or any type.
if (type !is Class<*>) {
if (type is ParameterizedType) {
checkParameterisedTypesConcrete(type.actualTypeArguments)
} else if (type != AnyType) {
throw NotSerializableException("Declared parameterised types containing $type as a parameter are not supported.")
private fun makeClassSerializer(clazz: Class<*>, type: Type, declaredType: Type): AMQPSerializer<Any> {
return serializersByType.computeIfAbsent(type) {
if (isPrimitive(clazz)) {
AMQPPrimitiveSerializer(clazz)
} else {
findCustomSerializer(clazz, declaredType) ?: run {
if (type.isArray()) {
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> {
return serializersByType.computeIfAbsent(clazz) {
if (isPrimitive(clazz)) {
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>? {
internal fun findCustomSerializer(clazz: Class<*>, declaredType: Type): AMQPSerializer<Any>? {
// e.g. Imagine if we provided a Map serializer this way, then it won't work if the declared type is AbstractMap, only Map.
// Otherwise it needs to inject additional schema for a RestrictedType source of the super type. Could be done, but do we need it?
for (customSerializer in customSerializers) {
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
}
private fun whitelisted(clazz: Class<*>): Boolean {
if (whitelist.hasListed(clazz) || hasAnnotationInHierarchy(clazz)) {
return true
} else {
throw NotSerializableException("Class $clazz is not on the whitelist or annotated with @CordaSerializable.")
private fun whitelisted(type: Type) {
val clazz = type.asClass()!!
if (!whitelist.hasListed(clazz) && !hasAnnotationInHierarchy(clazz)) {
throw NotSerializableException("Class $type is not on the whitelist or annotated with @CordaSerializable.")
}
}
// Recursively check the class, interfaces and superclasses for our annotation.
internal fun hasAnnotationInHierarchy(type: Class<*>): Boolean {
return type.isAnnotationPresent(CordaSerializable::class.java) ||
type.interfaces.any { it.isAnnotationPresent(CordaSerializable::class.java) || hasAnnotationInHierarchy(it) }
type.interfaces.any { hasAnnotationInHierarchy(it) }
|| (type.superclass != null && hasAnnotationInHierarchy(type.superclass))
}
@ -211,9 +257,16 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
}
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(
Boolean::class.java to "boolean",
@ -221,7 +274,7 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
UnsignedByte::class.java to "ubyte",
Short::class.java to "short",
UnsignedShort::class.java to "ushort",
Integer::class.java to "int",
Int::class.java to "int",
UnsignedInteger::class.java to "uint",
Long::class.java to "long",
UnsignedLong::class.java to "ulong",
@ -233,9 +286,36 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
Char::class.java to "char",
Date::class.java to "timestamp",
UUID::class.java to "uuid",
Binary::class.java to "binary",
ByteArray::class.java to "binary",
String::class.java to "string",
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 {
@ -246,4 +326,3 @@ class SerializerFactory(val whitelist: ClassWhitelist = AllWhitelist) {
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.serialization.amqp.*
import org.apache.qpid.proton.amqp.Binary
import org.apache.qpid.proton.codec.Data
import java.lang.reflect.Type
import java.security.PublicKey
class PublicKeySerializer : CustomSerializer.Implements<PublicKey>(PublicKey::class.java) {
/**
* 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 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) {
// 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 {
val A = input.readObject(obj, schema, ByteArray::class.java) as Binary
return Crypto.decodePublicKey(A.array)
val bits = input.readObject(obj, schema, ByteArray::class.java) as ByteArray
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.constructorForDeserialization
import net.corda.core.serialization.amqp.propertiesForSerialization
import net.corda.core.utilities.CordaRuntimeException
import net.corda.core.utilities.CordaThrowable
import net.corda.core.CordaRuntimeException
import net.corda.core.CordaThrowable
import java.io.NotSerializableException
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.MethodVisitor
import org.objectweb.asm.Opcodes.*
import org.objectweb.asm.Type
import java.lang.Character.*
import java.lang.Character.isJavaIdentifierPart
import java.lang.Character.isJavaIdentifierStart
import java.util.*
/**
@ -16,6 +19,7 @@ interface SimpleFieldAccess {
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.
* 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.
*/
class ClassCarpenter {
// TODO: Array types.
// TODO: Generics.
// TODO: Sandbox the generated code when a security manager is in use.
// TODO: Generate equals/hashCode.
// TODO: Support annotations.
// 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.
*/
class Schema(val name: String, fields: Map<String, Class<out Any?>>, val superclass: Schema? = null, val interfaces: List<Class<*>> = emptyList()) {
val fields = LinkedHashMap(fields) // Fix the order up front if the user didn't.
val descriptors = fields.map { it.key to Type.getDescriptor(it.value) }.toMap()
abstract class Schema(
val name: String,
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)
fun descriptorsIncludingSuperclasses(): Map<String, String> = (superclass?.descriptorsIncludingSuperclasses() ?: emptyMap()) + LinkedHashMap(descriptors)
/* Fix the order up front if the user didn't, inject the name into the field as it's
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.")
class InterfaceMismatch(msg: String) : RuntimeException(msg)
private val String.jvm: String get() = replace(".", "/")
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) {
fun load(name: String, bytes: ByteArray) = defineClass(name, bytes, 0, bytes.size)
}
private val classloader = CarpenterClassLoader()
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 */
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
* construct instances of the generated class.
@ -111,39 +229,67 @@ class ClassCarpenter {
hierarchy += cursor
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]!!
}
private fun generateClass(schema: Schema): Class<*> {
val jvmName = schema.name.jvm
private fun generateInterface(interfaceSchema: Schema): Class<*> {
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.
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES or ClassWriter.COMPUTE_MAXS)
with(cw) {
// public class Name implements SimpleFieldAccess {
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()
}
generator(cw, schema)
val clazz = classloader.load(schema.name, cw.toByteArray())
_loaded[schema.name] = clazz
return clazz
}
private fun ClassWriter.generateFields(schema: Schema) {
for ((name, desc) in schema.descriptors) {
visitField(ACC_PROTECTED + ACC_FINAL, name, desc, null, null).visitEnd()
}
schema.fields.forEach { it.value.generateField(this) }
}
private fun ClassWriter.generateToString(jvmName: String, schema: Schema) {
private fun ClassWriter.generateToString(schema: Schema) {
val toStringHelper = "com/google/common/base/MoreObjects\$ToStringHelper"
with(visitMethod(ACC_PUBLIC, "toString", "()Ljava/lang/String;", "", null)) {
visitCode()
@ -151,12 +297,11 @@ class ClassCarpenter {
visitLdcInsn(schema.name.split('.').last())
visitMethodInsn(INVOKESTATIC, "com/google/common/base/MoreObjects", "toStringHelper", "(Ljava/lang/String;)L$toStringHelper;", false)
// Call the add() methods.
for ((name, type) in schema.fieldsIncludingSuperclasses().entries) {
for ((name, field) in schema.fieldsIncludingSuperclasses().entries) {
visitLdcInsn(name)
visitVarInsn(ALOAD, 0) // this
visitFieldInsn(GETFIELD, jvmName, name, schema.descriptorsIncludingSuperclasses()[name])
val desc = if (type.isPrimitive) schema.descriptors[name] else "Ljava/lang/Object;"
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;$desc)L$toStringHelper;", false)
visitFieldInsn(GETFIELD, schema.jvmName, name, schema.descriptorsIncludingSuperclasses()[name])
visitMethodInsn(INVOKEVIRTUAL, toStringHelper, "add", "(Ljava/lang/String;${field.type})L$toStringHelper;", false)
}
// call toString() on the builder and return.
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) {
val descriptor = schema.descriptors[name]
with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + descriptor, null, null)) {
with(visitMethod(ACC_PUBLIC, "get" + name.capitalize(), "()" + type.descriptor, null, null)) {
type.addNullabilityAnnotation(this)
visitCode()
visitVarInsn(ALOAD, 0) // Load 'this'
visitFieldInsn(GETFIELD, jvmName, name, descriptor)
when (type) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitInsn(IRETURN)
visitFieldInsn(GETFIELD, schema.jvmName, name, type.descriptor)
when (type.field) {
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.Double.TYPE -> visitInsn(DRETURN)
java.lang.Float.TYPE -> visitInsn(FRETURN)
@ -202,9 +348,29 @@ class ClassCarpenter {
}
}
private fun ClassWriter.generateConstructor(jvmName: String, schema: Schema) {
with(visitMethod(ACC_PUBLIC, "<init>", "(" + schema.descriptorsIncludingSuperclasses().values.joinToString("") + ")V", null, null)) {
private fun ClassWriter.generateAbstractGetters(schema: Schema) {
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()
// Calculate the super call.
val superclassFields = schema.superclass?.fieldsIncludingSuperclasses() ?: emptyMap()
visitVarInsn(ALOAD, 0)
@ -217,14 +383,15 @@ class ClassCarpenter {
val superDesc = schema.superclass.descriptorsIncludingSuperclasses().values.joinToString("")
visitMethodInsn(INVOKESPECIAL, schema.superclass.name.jvm, "<init>", "($superDesc)V", false)
}
// Assign the fields from parameters.
var slot = 1 + superclassFields.size
for ((name, type) in schema.fields.entries) {
if (type.isArray)
throw UnsupportedOperationException("Array types are not implemented yet")
for ((name, field) in schema.fields.entries) {
field.nullTest(this, slot)
visitVarInsn(ALOAD, 0) // Load 'this' onto the stack
slot += load(slot, type) // Load the contents of the parameter onto the stack.
visitFieldInsn(PUTFIELD, jvmName, name, schema.descriptors[name])
slot += load(slot, field) // Load the contents of the parameter onto the stack.
visitFieldInsn(PUTFIELD, schema.jvmName, name, field.descriptor)
}
visitInsn(RETURN)
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: Class<out Any?>): Int {
when (type) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE, TYPE -> visitVarInsn(ILOAD, slot)
private fun MethodVisitor.load(slot: Int, type: Field): Int {
when (type.field) {
java.lang.Boolean.TYPE, Integer.TYPE, java.lang.Short.TYPE, java.lang.Byte.TYPE,
java.lang.Character.TYPE -> visitVarInsn(ILOAD, slot)
java.lang.Long.TYPE -> visitVarInsn(LLOAD, slot)
java.lang.Double.TYPE -> visitVarInsn(DLOAD, slot)
java.lang.Float.TYPE -> visitVarInsn(FLOAD, slot)
else -> visitVarInsn(ALOAD, slot)
}
return when (type) {
return when (type.field) {
java.lang.Long.TYPE, java.lang.Double.TYPE -> 2
else -> 1
}
}
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)
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" } }
@ -257,13 +424,18 @@ class ClassCarpenter {
// actually called, which is a bit too dynamic for my tastes.
val allFields = schema.fieldsIncludingSuperclasses()
for (itf in schema.interfaces) {
for (method in itf.methods) {
itf.methods.forEach {
val fieldNameFromItf = when {
method.name.startsWith("get") -> method.name.substring(3).decapitalize()
else -> throw InterfaceMismatch("Requested interfaces must consist only of methods that start with 'get': ${itf.name}.${method.name}")
it.name.startsWith("get") -> it.name.substring(3).decapitalize()
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.NamedByHash
import net.corda.core.contracts.TransactionResolutionException
import net.corda.core.contracts.TransactionVerificationException
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.isFulfilledBy
import net.corda.core.crypto.keys
import net.corda.core.node.ServiceHub
import net.corda.core.serialization.CordaSerializable
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)
/**
* Calls [verifySignatures] to check all required signatures are present, and then calls
* [WireTransaction.toLedgerTransaction] with the passed in [ServiceHub] to resolve the dependencies,
* returning an unverified LedgerTransaction.
* Checks the transaction's signatures are valid, optionally calls [verifySignatures] to check
* all required signatures are present, and then calls [WireTransaction.toLedgerTransaction]
* 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 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.
*/
@Throws(AttachmentResolutionException::class, TransactionResolutionException::class, SignatureException::class)
fun toLedgerTransaction(services: ServiceHub) = verifySignatures().toLedgerTransaction(services)
@JvmOverloads
@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)"
}

View File

@ -3,11 +3,13 @@ package net.corda.core.transactions
import co.paralleluniverse.strands.Strand
import net.corda.core.contracts.*
import net.corda.core.crypto.*
import net.corda.core.internal.FlowStateMachine
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 java.security.KeyPair
import java.security.PublicKey
import java.security.SignatureException
import java.time.Duration
import java.time.Instant
import java.util.*
@ -37,46 +39,22 @@ open class TransactionBuilder(
protected val outputs: MutableList<TransactionState<ContractState>> = arrayListOf(),
protected val commands: MutableList<Command> = arrayListOf(),
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())
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.
*/
fun copy(): TransactionBuilder =
TransactionBuilder(
type = type,
notary = notary,
inputs = ArrayList(inputs),
attachments = ArrayList(attachments),
outputs = ArrayList(outputs),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
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
}
fun copy() = TransactionBuilder(
type = type,
notary = notary,
inputs = ArrayList(inputs),
attachments = ArrayList(attachments),
outputs = ArrayList(outputs),
commands = ArrayList(commands),
signers = LinkedHashSet(signers),
window = window
)
// DOCSTART 1
/** 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) {
when (t) {
is StateAndRef<*> -> addInputState(t)
is SecureHash -> addAttachment(t)
is TransactionState<*> -> addOutputState(t)
is ContractState -> addOutputState(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 TimeWindow -> setTimeWindow(t)
else -> throw IllegalArgumentException("Wrong argument type: ${t.javaClass}")
}
}
@ -95,49 +75,101 @@ open class TransactionBuilder(
}
// 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! */
@Deprecated("Signatures should be gathered on a SignedTransaction instead.")
protected val currentSigs = arrayListOf<DigitalSignature.WithKey>()
@Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun signWith(key: KeyPair): TransactionBuilder {
check(currentSigs.none { it.by == key.public }) { "This partial transaction was already signed by ${key.public}" }
val data = toWireTransaction().id
addSignatureUnchecked(key.sign(data.bytes))
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. */
@Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun addSignatureUnchecked(sig: DigitalSignature.WithKey): TransactionBuilder {
currentSigs.add(sig)
return this
}
fun toWireTransaction() = WireTransaction(ArrayList(inputs), ArrayList(attachments),
ArrayList(outputs), ArrayList(commands), notary, signers.toList(), type, timeWindow)
@Deprecated("Use ServiceHub.signInitialTransaction() instead.")
fun toSignedTransaction(checkSufficientSignatures: Boolean = true): SignedTransaction {
if (checkSufficientSignatures) {
val gotKeys = currentSigs.map { it.by }.toSet()
@ -149,48 +181,28 @@ open class TransactionBuilder(
return SignedTransaction(wtx.serialize(), ArrayList(currentSigs))
}
open fun addInputState(stateAndRef: StateAndRef<*>) {
check(currentSigs.isEmpty())
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)
/**
* 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.
*/
@Deprecated("Use WireTransaction.checkSignature() instead.")
fun checkAndAddSignature(sig: DigitalSignature.WithKey) {
checkSignature(sig)
addSignatureUnchecked(sig)
}
fun addAttachment(attachmentId: SecureHash) {
check(currentSigs.isEmpty())
attachments.add(attachmentId)
/**
* 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.
*/
@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 net.corda.core.contracts.*
import net.corda.core.crypto.DigitalSignature
import net.corda.core.crypto.MerkleTree
import net.corda.core.crypto.SecureHash
import net.corda.core.crypto.keys
import net.corda.core.identity.Party
import net.corda.core.indexOfOrThrow
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.utilities.Emoji
import java.security.PublicKey
import java.security.SignatureException
import java.util.function.Predicate
/**
@ -73,7 +76,7 @@ class WireTransaction(
fun toLedgerTransaction(services: ServicesForResolution): LedgerTransaction {
return toLedgerTransaction(
resolveIdentity = { services.identityService.partyFromKey(it) },
resolveAttachment = { services.storageService.attachments.openAttachment(it) },
resolveAttachment = { services.attachments.openAttachment(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 {
val buf = StringBuilder()
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 net.corda.core.serialization.CordaSerializable
import java.io.ByteArrayInputStream
import java.util.*
@ -11,12 +14,13 @@ import java.util.*
*/
@CordaSerializable
open class OpaqueBytes(val bytes: ByteArray) {
init {
check(bytes.isNotEmpty())
companion object {
@JvmStatic
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
}
companion object {
fun of(vararg b: Byte) = OpaqueBytes(byteArrayOf(*b))
init {
check(bytes.isNotEmpty())
}
override fun equals(other: Any?): Boolean {

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