mirror of
https://github.com/corda/corda.git
synced 2024-12-27 08:22:35 +00:00
Merge remote-tracking branch 'open/master' into aslemmer-enterprise-merge-july-13
This commit is contained in:
commit
eac7db295c
2
.gitignore
vendored
2
.gitignore
vendored
@ -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
2
.idea/compiler.xml
generated
@ -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" />
|
||||
|
@ -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
|
||||
|
27
build.gradle
27
build.gradle
@ -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')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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')
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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) =
|
||||
|
@ -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> {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() }
|
||||
|
@ -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
|
||||
) {
|
||||
|
@ -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)
|
||||
|
@ -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 -> {
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
@ -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()
|
||||
|
||||
|
@ -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
|
||||
|
@ -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/>
|
||||
|
@ -1,4 +1,4 @@
|
||||
gradlePluginsVersion=0.12.4
|
||||
gradlePluginsVersion=0.13.2
|
||||
kotlinVersion=1.1.1
|
||||
guavaVersion=21.0
|
||||
bouncycastleVersion=1.57
|
||||
|
@ -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"
|
||||
|
@ -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";
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,9 @@
|
||||
package net.corda.cordform;
|
||||
|
||||
import com.typesafe.config.Config;
|
||||
|
||||
public interface NodeDefinition {
|
||||
String getName();
|
||||
|
||||
Config getConfig();
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package net.corda.core.utilities
|
||||
package net.corda.core
|
||||
|
||||
import net.corda.core.serialization.CordaSerializable
|
||||
import java.util.*
|
@ -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
|
||||
|
@ -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
|
||||
}
|
@ -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() }
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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()
|
@ -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)
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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).
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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()
|
@ -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
|
@ -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
|
||||
|
||||
/**
|
@ -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")
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
@ -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))
|
@ -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
|
||||
/**
|
||||
|
@ -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
|
||||
/**
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
||||
|
@ -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))
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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.
|
||||
*/
|
||||
|
@ -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>>
|
||||
}
|
@ -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>
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>() {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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
|
||||
|
@ -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)) {
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)) {
|
||||
|
@ -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) }
|
||||
}
|
||||
|
||||
|
||||
|
@ -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 -> "�"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 = "?"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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)
|
@ -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 })
|
@ -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)
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
@ -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())
|
||||
}
|
||||
}
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)"
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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:")
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user